chain/mempool: store peer id and punish invalid orphans.

This commit is contained in:
Christopher Jeffrey 2017-05-16 00:55:54 -07:00
parent 0b13452df1
commit 0ceca23cb5
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
7 changed files with 520 additions and 351 deletions

View File

@ -33,12 +33,11 @@ var VerifyResult = errors.VerifyResult;
* @param {String?} options.name - Database name. * @param {String?} options.name - Database name.
* @param {String?} options.location - Database file location. * @param {String?} options.location - Database file location.
* @param {String?} options.db - Database backend (`"leveldb"` by default). * @param {String?} options.db - Database backend (`"leveldb"` by default).
* @param {Number?} options.orphanLimit * @param {Number?} options.maxOrphans
* @param {Boolean?} options.spv * @param {Boolean?} options.spv
* @property {Boolean} loaded * @property {Boolean} loaded
* @property {ChainDB} db - Note that Chain `options` will be passed * @property {ChainDB} db - Note that Chain `options` will be passed
* to the instantiated ChainDB. * to the instantiated ChainDB.
* @property {Number} total
* @property {Lock} locker * @property {Lock} locker
* @property {Object} invalid * @property {Object} invalid
* @property {ChainEntry?} tip * @property {ChainEntry?} tip
@ -80,13 +79,10 @@ function Chain(options) {
this.tip = null; this.tip = null;
this.height = -1; this.height = -1;
this.synced = false; this.synced = false;
this.total = 0;
this.startTime = util.hrtime();
this.orphanMap = {}; this.orphanMap = {};
this.orphanPrev = {}; this.orphanPrev = {};
this.orphanCount = 0; this.orphanCount = 0;
this.orphanSize = 0;
this.db = new ChainDB(this); this.db = new ChainDB(this);
} }
@ -224,6 +220,7 @@ Chain.prototype.isGenesis = function isGenesis(block) {
*/ */
Chain.prototype.verify = co(function* verify(block, prev, flags) { Chain.prototype.verify = co(function* verify(block, prev, flags) {
var hash = block.hash('hex');
var ret = new VerifyResult(); var ret = new VerifyResult();
var now = this.network.now(); var now = this.network.now();
var height = prev.height + 1; var height = prev.height + 1;
@ -236,6 +233,14 @@ Chain.prototype.verify = co(function* verify(block, prev, flags) {
if (block.prevBlock !== prev.hash) if (block.prevBlock !== prev.hash)
throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); throw new VerifyError(block, 'invalid', 'bad-prevblk', 0);
// Verify a checkpoint if there is one.
if (!this.verifyCheckpoint(prev, hash)) {
throw new VerifyError(block,
'checkpoint',
'checkpoint mismatch',
100);
}
// Skip everything when using checkpoints. // Skip everything when using checkpoints.
// We can do this safely because every // We can do this safely because every
// block in between each checkpoint was // block in between each checkpoint was
@ -842,6 +847,7 @@ Chain.prototype.disconnect = co(function* disconnect(entry) {
this.height = prev.height; this.height = prev.height;
this.emit('tip', prev); this.emit('tip', prev);
yield this.fire('disconnect', entry, block, view); yield this.fire('disconnect', entry, block, view);
}); });
@ -891,6 +897,7 @@ Chain.prototype.reconnect = co(function* reconnect(entry) {
this.emit('tip', entry); this.emit('tip', entry);
this.emit('reconnect', entry, block); this.emit('reconnect', entry, block);
yield this.fire('connect', entry, block, result.view); yield this.fire('connect', entry, block, result.view);
}); });
@ -958,6 +965,7 @@ Chain.prototype.setBestChain = co(function* setBestChain(entry, block, prev, fla
this.emit('tip', entry); this.emit('tip', entry);
this.emit('block', block, entry); this.emit('block', block, entry);
yield this.fire('connect', entry, block, result.view); yield this.fire('connect', entry, block, result.view);
}); });
@ -1173,15 +1181,16 @@ Chain.prototype.scan = co(function* scan(start, filter, iter) {
* Add a block to the chain, perform all necessary verification. * Add a block to the chain, perform all necessary verification.
* @method * @method
* @param {Block} block * @param {Block} block
* @param {Number} flags * @param {Number?} flags
* @param {Number?} id
* @returns {Promise} * @returns {Promise}
*/ */
Chain.prototype.add = co(function* add(block, flags) { Chain.prototype.add = co(function* add(block, flags, id) {
var hash = block.hash('hex'); var hash = block.hash('hex');
var unlock = yield this.locker.lock(hash); var unlock = yield this.locker.lock(hash);
try { try {
return yield this._add(block, flags); return yield this._add(block, flags, id);
} finally { } finally {
unlock(); unlock();
} }
@ -1192,146 +1201,181 @@ Chain.prototype.add = co(function* add(block, flags) {
* @method * @method
* @private * @private
* @param {Block} block * @param {Block} block
* @param {Number} flags * @param {Number?} flags
* @param {Number?} id
* @returns {Promise} * @returns {Promise}
*/ */
Chain.prototype._add = co(function* add(block, flags) { Chain.prototype._add = co(function* add(block, flags, id) {
var initial = true; var hash = block.hash('hex');
var result = null; var entry, prev;
var hash, entry, prev;
if (flags == null) if (flags == null)
flags = common.flags.DEFAULT_FLAGS; flags = common.flags.DEFAULT_FLAGS;
assert(block); if (id == null)
id = -1;
while (block) { // Special case for genesis block.
hash = block.hash('hex'); if (hash === this.network.genesis.hash) {
this.logger.debug('Saw genesis block: %s.', block.rhash());
// Mark the start time. throw new VerifyError(block, 'duplicate', 'duplicate', 0);
this.mark();
// Special case for genesis block.
if (hash === this.network.genesis.hash) {
this.logger.debug('Saw genesis block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// Do we already have this block in the queue?
if (this.hasPending(hash)) {
this.logger.debug('Already have pending block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// If the block is already known to be
// an orphan, ignore it.
if (this.hasOrphan(hash)) {
this.logger.debug('Already have orphan block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// Do not revalidate known invalid blocks.
if (this.hasInvalid(hash, block)) {
this.logger.debug('Invalid ancestors for block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 100);
}
// Check the POW before doing anything.
if (flags & common.flags.VERIFY_POW) {
if (!block.verifyPOW())
throw new VerifyError(block, 'invalid', 'high-hash', 50);
}
// Do we already have this block?
if (yield this.db.hasEntry(hash)) {
this.logger.debug('Already have block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// Find the previous block entry.
prev = yield this.db.getEntry(block.prevBlock);
// If previous block wasn't ever seen,
// add it current to orphans and break.
if (!prev) {
assert(initial);
assert(!result);
this.storeOrphan(block);
break;
}
// Verify a checkpoint if there is one.
if (!this.verifyCheckpoint(prev, hash)) {
throw new VerifyError(block,
'checkpoint',
'checkpoint mismatch',
100);
}
// Explanation: we try to keep as much data
// off the javascript heap as possible. Blocks
// in the future may be 8mb or 20mb, who knows.
// In fullnode-mode we store the blocks in
// "compact" form (the headers plus the raw
// Buffer object) until they're ready to be
// fully validated here. They are deserialized,
// validated, and emitted. Hopefully the deserialized
// blocks get cleaned up by the GC quickly.
if (block.memory) {
try {
block = block.toBlock();
} catch (e) {
this.logger.error(e);
throw new VerifyError(block,
'malformed',
'error parsing message',
10);
}
}
// Create a new chain entry.
entry = ChainEntry.fromBlock(this, block, prev);
// The block is on a alternate 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) {
// Save block to an alternate chain.
yield this.saveAlternate(entry, block, prev, flags);
} else {
// Attempt to add block to the chain index.
yield this.setBestChain(entry, block, prev, flags);
}
// Emit the resolved orphan.
if (!initial) {
this.logger.debug(
'Orphan block was resolved: %s (%d).',
block.rhash(), entry.height);
this.emit('resolved', block, entry);
}
// Keep track of stats.
this.finish(block, entry);
// Try to resolve orphan chain.
block = this.resolveOrphan(hash);
initial = false;
if (!result)
result = entry;
} }
// Failsafe for large orphan chains. Do not // Do we already have this block in the queue?
// allow more than 20mb stored in memory. if (this.hasPending(hash)) {
this.pruneOrphans(); this.logger.debug('Already have pending block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// If the block is already known to be
// an orphan, ignore it.
if (this.hasOrphan(hash)) {
this.logger.debug('Already have orphan block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// Do not revalidate known invalid blocks.
if (this.hasInvalid(hash, block)) {
this.logger.debug('Invalid ancestors for block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 100);
}
// Check the POW before doing anything.
if (flags & common.flags.VERIFY_POW) {
if (!block.verifyPOW())
throw new VerifyError(block, 'invalid', 'high-hash', 50);
}
// Do we already have this block?
if (yield this.db.hasEntry(hash)) {
this.logger.debug('Already have block: %s.', block.rhash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
// Find the previous block entry.
prev = yield this.db.getEntry(block.prevBlock);
// If previous block wasn't ever seen,
// add it current to orphans and return.
if (!prev) {
this.storeOrphan(block, flags, id);
return null;
}
// Connect the block.
entry = yield this.connect(prev, block, flags);
// Handle any orphans.
if (this.hasNextOrphan(hash))
yield this.handleOrphans(entry);
return entry;
});
/**
* Connect block to chain.
* @method
* @private
* @param {ChainEntry} prev
* @param {Block} block
* @param {Number} flags
* @returns {Promise}
*/
Chain.prototype.connect = co(function* connect(prev, block, flags) {
var start = util.hrtime();
var entry;
// Sanity check.
assert(block.prevBlock === prev.hash);
// Explanation: we try to keep as much data
// off the javascript heap as possible. Blocks
// in the future may be 8mb or 20mb, who knows.
// In fullnode-mode we store the blocks in
// "compact" form (the headers plus the raw
// Buffer object) until they're ready to be
// fully validated here. They are deserialized,
// validated, and connected. Hopefully the
// deserialized blocks get cleaned up by the
// GC quickly.
if (block.memory) {
try {
block = block.toBlock();
} catch (e) {
this.logger.error(e);
throw new VerifyError(block,
'malformed',
'error parsing message',
10);
}
}
// Create a new chain entry.
entry = ChainEntry.fromBlock(this, block, prev);
// The block is on a alternate 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) {
// Save block to an alternate chain.
yield this.saveAlternate(entry, block, prev, flags);
} else {
// Attempt to add block to the chain index.
yield this.setBestChain(entry, block, prev, flags);
}
// Keep track of stats.
this.logStatus(start, block, entry);
// Check sync state. // Check sync state.
this.maybeSync(); this.maybeSync();
return result; return entry;
});
/**
* Handle orphans.
* @method
* @private
* @param {ChainEntry} entry
* @returns {Promise}
*/
Chain.prototype.handleOrphans = co(function* handleOrphans(entry) {
var orphan = this.resolveOrphan(entry.hash);
var block, flags, id;
while (orphan) {
block = orphan.block;
flags = orphan.flags;
id = orphan.id;
try {
entry = yield this.connect(entry, block, flags);
} catch (err) {
if (err.type === 'VerifyError') {
this.logger.warning(
'Could not resolve orphan block %s: %s.',
block.rhash(), err.message);
this.emit('bad orphan', err, id);
break;
}
throw err;
}
this.logger.debug(
'Orphan block was resolved: %s (%d).',
block.rhash(), entry.height);
this.emit('resolved', block, entry);
orphan = this.resolveOrphan(entry.hash);
}
}); });
/** /**
@ -1344,43 +1388,37 @@ Chain.prototype.isSlow = function isSlow() {
if (this.options.spv) if (this.options.spv)
return false; return false;
if (this.total === 1 || this.total % 20 === 0) if (this.synced)
return true; return true;
return this.synced || this.height >= this.network.block.slowHeight; if (this.height === 1 || this.height % 20 === 0)
}; return true;
/** if (this.height >= this.network.block.slowHeight)
* Mark the start time for block processing. return true;
* @private
*/
Chain.prototype.mark = function mark() { return false;
this.startTime = util.hrtime();
}; };
/** /**
* Calculate the time difference from * Calculate the time difference from
* start time and log block. * start time and log block.
* @private * @private
* @param {Array} start
* @param {Block} block * @param {Block} block
* @param {ChainEntry} entry * @param {ChainEntry} entry
*/ */
Chain.prototype.finish = function finish(block, entry) { Chain.prototype.logStatus = function logStatus(start, block, entry) {
var elapsed; var elapsed;
// Keep track of total blocks handled.
this.total += 1;
if (!this.isSlow()) if (!this.isSlow())
return; return;
// Report memory for debugging. // Report memory for debugging.
util.gc();
this.logger.memory(); this.logger.memory();
elapsed = util.hrtime(this.startTime); elapsed = util.hrtime(start);
this.logger.info( this.logger.info(
'Block %s (%d) added to chain (size=%d txs=%d time=%d).', 'Block %s (%d) added to chain (size=%d txs=%d time=%d).',
@ -1443,29 +1481,32 @@ Chain.prototype.verifyCheckpoint = function verifyCheckpoint(prev, hash) {
* Store an orphan. * Store an orphan.
* @private * @private
* @param {Block} block * @param {Block} block
* @param {Number?} flags
* @param {Number?} id
*/ */
Chain.prototype.storeOrphan = function storeOrphan(block) { Chain.prototype.storeOrphan = function storeOrphan(block, flags, id) {
var hash = block.hash('hex'); var hash = block.hash('hex');
var height = block.getCoinbaseHeight(); var height = block.getCoinbaseHeight();
var orphan = this.orphanPrev[block.prevBlock]; var orphan = this.orphanPrev[block.prevBlock];
// The orphan chain forked. // The orphan chain forked.
if (orphan) { if (orphan) {
assert(orphan.hash('hex') !== hash); assert(orphan.block.hash('hex') !== hash);
assert(orphan.prevBlock === block.prevBlock); assert(orphan.block.prevBlock === block.prevBlock);
this.logger.warning( this.logger.warning(
'Removing forked orphan block: %s (%d).', 'Removing forked orphan block: %s (%d).',
orphan.rhash(), height); orphan.block.rhash(), height);
this.resolveOrphan(block.prevBlock); this.removeOrphan(orphan.block.hash('hex'));
} }
this.orphanCount++; this.limitOrphans();
this.orphanSize += block.getSize();
this.orphanPrev[block.prevBlock] = block; orphan = new Orphan(block, flags, id);
this.orphanMap[hash] = block;
this.addOrphan(orphan);
this.logger.debug( this.logger.debug(
'Storing orphan block: %s (%d).', 'Storing orphan block: %s (%d).',
@ -1474,26 +1515,75 @@ Chain.prototype.storeOrphan = function storeOrphan(block) {
this.emit('orphan', block); this.emit('orphan', block);
}; };
/**
* Add an orphan.
* @private
* @param {Orphan} orphan
* @returns {Orphan}
*/
Chain.prototype.addOrphan = function addOrphan(orphan) {
var block = orphan.block;
var hash = block.hash('hex');
assert(!this.orphanMap[hash]);
assert(!this.orphanPrev[block.prevBlock]);
assert(this.orphanCount >= 0);
this.orphanMap[hash] = orphan;
this.orphanPrev[block.prevBlock] = orphan;
this.orphanCount += 1;
return orphan;
};
/**
* Remove an orphan.
* @private
* @param {Hash} hash
* @returns {Orphan}
*/
Chain.prototype.removeOrphan = function removeOrphan(hash) {
var orphan = this.orphanMap[hash];
var block = orphan.block;
assert(this.orphanMap[hash]);
assert(this.orphanPrev[block.prevBlock]);
assert(this.orphanCount > 0);
delete this.orphanMap[hash];
delete this.orphanPrev[block.prevBlock];
this.orphanCount -= 1;
return orphan;
};
/**
* Test whether a hash would resolve the next orphan.
* @private
* @param {Hash} hash - Previous block hash.
* @returns {Boolean}
*/
Chain.prototype.hasNextOrphan = function hasNextOrphan(hash) {
return this.orphanPrev[hash] != null;
};
/** /**
* Resolve an orphan. * Resolve an orphan.
* @private * @private
* @param {Hash} hash - Previous block hash. * @param {Hash} hash - Previous block hash.
* @returns {Block} * @returns {Orphan}
*/ */
Chain.prototype.resolveOrphan = function resolveOrphan(hash) { Chain.prototype.resolveOrphan = function resolveOrphan(hash) {
var block = this.orphanPrev[hash]; var orphan = this.orphanPrev[hash];
if (!block) if (!orphan)
return; return;
delete this.orphanMap[block.hash('hex')]; return this.removeOrphan(orphan.block.hash('hex'));
delete this.orphanPrev[hash];
this.orphanCount--;
this.orphanSize -= block.getSize();
return block;
}; };
/** /**
@ -1502,18 +1592,15 @@ Chain.prototype.resolveOrphan = function resolveOrphan(hash) {
Chain.prototype.purgeOrphans = function purgeOrphans() { Chain.prototype.purgeOrphans = function purgeOrphans() {
var count = this.orphanCount; var count = this.orphanCount;
var size = this.orphanSize;
if (count === 0) if (count === 0)
return; return;
this.orphanPrev = {};
this.orphanMap = {}; this.orphanMap = {};
this.orphanPrev = {};
this.orphanCount = 0; this.orphanCount = 0;
this.orphanSize = 0;
this.logger.debug('Purged %d orphans (%dmb).', this.logger.debug('Purged %d orphans.', count);
count, util.mb(size));
}; };
/** /**
@ -1521,59 +1608,38 @@ Chain.prototype.purgeOrphans = function purgeOrphans() {
* coinbase height (likely to be the peer's tip). * coinbase height (likely to be the peer's tip).
*/ */
Chain.prototype.pruneOrphans = function pruneOrphans() { Chain.prototype.limitOrphans = function limitOrphans() {
var i, hashes, hash, orphan, height; var now = util.now();
var bestOrphan, bestHeight, lastOrphan; var hashes = Object.keys(this.orphanMap);
var total = 0;
if (this.orphanSize <= this.options.orphanLimit) var i, hash, orphan, oldest;
return false;
this.logger.warning('Pruning %d (%dmb) orphans!',
this.orphanCount, util.mb(this.orphanSize));
hashes = Object.keys(this.orphanPrev);
if (hashes.length === 0)
return false;
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
orphan = this.orphanPrev[hash];
height = orphan.getCoinbaseHeight();
delete this.orphanPrev[hash];
if (!bestOrphan || height > bestHeight) {
bestOrphan = orphan;
bestHeight = height;
}
lastOrphan = orphan;
}
// Save the best for last... or the
// last for best in this case.
if (bestHeight === -1)
bestOrphan = lastOrphan;
hashes = Object.keys(this.orphanMap);
for (i = 0; i < hashes.length; i++) { for (i = 0; i < hashes.length; i++) {
hash = hashes[i]; hash = hashes[i];
orphan = this.orphanMap[hash]; orphan = this.orphanMap[hash];
delete this.orphanMap[hash]; if (now < orphan.ts + 60 * 60) {
if (!oldest || orphan.ts < oldest.ts)
oldest = orphan;
continue;
}
if (orphan !== bestOrphan) this.removeOrphan(hash);
this.emit('unresolved', orphan);
total++;
} }
this.orphanPrev[bestOrphan.prevBlock] = bestOrphan; if (this.orphanCount >= this.options.maxOrphans) {
this.orphanMap[bestOrphan.hash('hex')] = bestOrphan; if (total === 0 && oldest) {
this.orphanCount = 1; hash = oldest.block.hash('hex');
this.orphanSize = bestOrphan.getSize(); this.removeOrphan(hash);
}
}
return true; if (total > 0)
this.logger.warning('Pruned %d orphans!', total);
return total;
}; };
/** /**
@ -1850,13 +1916,18 @@ Chain.prototype._getLocator = co(function* getLocator(start) {
*/ */
Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) {
var root; var root, orphan;
assert(hash); assert(hash);
while (this.orphanMap[hash]) { for (;;) {
orphan = this.orphanMap[hash];
if (!orphan)
break;
root = hash; root = hash;
hash = this.orphanMap[hash].prevBlock; hash = orphan.block.prevBlock;
} }
return root; return root;
@ -2330,7 +2401,7 @@ function ChainOptions(options) {
this.coinCache = 0; this.coinCache = 0;
this.entryCache = 5000; this.entryCache = 5000;
this.orphanLimit = 20 << 20; this.maxOrphans = 20;
this.checkpoints = true; this.checkpoints = true;
if (options) if (options)
@ -2428,9 +2499,9 @@ ChainOptions.prototype.fromOptions = function fromOptions(options) {
this.entryCache = options.entryCache; this.entryCache = options.entryCache;
} }
if (options.orphanLimit != null) { if (options.maxOrphans != null) {
assert(util.isNumber(options.orphanLimit)); assert(util.isNumber(options.maxOrphans));
this.orphanLimit = options.orphanLimit; this.maxOrphans = options.maxOrphans;
} }
if (options.checkpoints != null) { if (options.checkpoints != null) {
@ -2555,6 +2626,19 @@ function ContextResult(view, state) {
this.state = state; this.state = state;
} }
/**
* Orphan
* @constructor
* @ignore
*/
function Orphan(block, flags, id) {
this.block = block;
this.flags = flags;
this.id = id;
this.ts = util.now();
}
/* /*
* Expose * Expose
*/ */

View File

@ -292,7 +292,7 @@ Mempool.prototype._removeBlock = co(function* removeBlock(block, txs) {
continue; continue;
try { try {
yield this.insertTX(tx); yield this.insertTX(tx, -1);
total++; total++;
} catch (e) { } catch (e) {
this.emit('error', e); this.emit('error', e);
@ -448,25 +448,6 @@ Mempool.prototype.limitSize = function limitSize(added) {
return !this.hasEntry(added); return !this.hasEntry(added);
}; };
/**
* Purge orphan transactions from the mempool.
*/
Mempool.prototype.limitOrphans = function limitOrphans() {
var orphans = Object.keys(this.orphans);
var i, hash;
while (this.totalOrphans > this.options.maxOrphans) {
i = crypto.randomRange(0, orphans.length);
hash = orphans[i];
orphans.splice(i, 1);
this.logger.spam('Removing orphan %s from mempool.', util.revHex(hash));
this.removeOrphan(hash);
}
};
/** /**
* Retrieve a transaction from the mempool. * Retrieve a transaction from the mempool.
* @param {Hash} hash * @param {Hash} hash
@ -721,14 +702,15 @@ Mempool.prototype.hasReject = function hasReject(hash) {
* fully processed. * fully processed.
* @method * @method
* @param {TX} tx * @param {TX} tx
* @param {Number?} id
* @returns {Promise} * @returns {Promise}
*/ */
Mempool.prototype.addTX = co(function* addTX(tx) { Mempool.prototype.addTX = co(function* addTX(tx, id) {
var hash = tx.hash('hex'); var hash = tx.hash('hex');
var unlock = yield this.locker.lock(hash); var unlock = yield this.locker.lock(hash);
try { try {
return yield this._addTX(tx); return yield this._addTX(tx, id);
} finally { } finally {
unlock(); unlock();
} }
@ -739,14 +721,18 @@ Mempool.prototype.addTX = co(function* addTX(tx) {
* @method * @method
* @private * @private
* @param {TX} tx * @param {TX} tx
* @param {Number?} id
* @returns {Promise} * @returns {Promise}
*/ */
Mempool.prototype._addTX = co(function* _addTX(tx) { Mempool.prototype._addTX = co(function* _addTX(tx, id) {
var missing; var missing;
if (id == null)
id = -1;
try { try {
missing = yield this.insertTX(tx); missing = yield this.insertTX(tx, id);
} catch (err) { } catch (err) {
if (err.type === 'VerifyError') { if (err.type === 'VerifyError') {
if (!tx.hasWitness() && !err.malleated) if (!tx.hasWitness() && !err.malleated)
@ -768,11 +754,13 @@ Mempool.prototype._addTX = co(function* _addTX(tx) {
* @method * @method
* @private * @private
* @param {TX} tx * @param {TX} tx
* @param {Number?} id
* @returns {Promise} * @returns {Promise}
*/ */
Mempool.prototype.insertTX = co(function* insertTX(tx) { Mempool.prototype.insertTX = co(function* insertTX(tx, id) {
var lockFlags = common.lockFlags.STANDARD_LOCKTIME_FLAGS; var lockFlags = common.lockFlags.STANDARD_LOCKTIME_FLAGS;
var height = this.chain.height;
var hash = tx.hash('hex'); var hash = tx.hash('hex');
var ret = new VerifyResult(); var ret = new VerifyResult();
var entry, view, missing; var entry, view, missing;
@ -883,11 +871,11 @@ Mempool.prototype.insertTX = co(function* insertTX(tx) {
// Maybe store as an orphan. // Maybe store as an orphan.
if (missing) if (missing)
return this.storeOrphan(tx, missing); return this.storeOrphan(tx, missing, id);
// Create a new mempool entry // Create a new mempool entry
// at current chain height. // at current chain height.
entry = MempoolEntry.fromTX(tx, view, this.chain.height); entry = MempoolEntry.fromTX(tx, view, height);
// Contextual verification. // Contextual verification.
yield this.verify(entry, view); yield this.verify(entry, view);
@ -902,6 +890,8 @@ Mempool.prototype.insertTX = co(function* insertTX(tx) {
'mempool full', 'mempool full',
0); 0);
} }
return null;
}); });
/** /**
@ -1498,9 +1488,11 @@ Mempool.prototype.hasOrphan = function hasOrphan(hash) {
/** /**
* Store an orphaned transaction. * Store an orphaned transaction.
* @param {TX} tx * @param {TX} tx
* @param {Hash[]} missing
* @param {Number} id
*/ */
Mempool.prototype.storeOrphan = function storeOrphan(tx, missing) { Mempool.prototype.storeOrphan = function storeOrphan(tx, missing, id) {
var hash = tx.hash('hex'); var hash = tx.hash('hex');
var i, prev; var i, prev;
@ -1520,60 +1512,73 @@ Mempool.prototype.storeOrphan = function storeOrphan(tx, missing) {
} }
} }
if (this.options.maxOrphans === 0)
return [];
this.limitOrphans();
for (i = 0; i < missing.length; i++) { for (i = 0; i < missing.length; i++) {
prev = missing[i]; prev = missing[i];
if (!this.waiting[prev]) if (!this.waiting[prev])
this.waiting[prev] = []; this.waiting[prev] = new Map();
this.waiting[prev].push(hash); this.waiting[prev].insert(hash);
} }
this.orphans[hash] = new Orphan(tx, missing.length); this.orphans[hash] = new Orphan(tx, missing.length, id);
this.totalOrphans++; this.totalOrphans++;
this.logger.debug('Added orphan %s to mempool.', tx.txid()); this.logger.debug('Added orphan %s to mempool.', tx.txid());
this.emit('add orphan', tx); this.emit('add orphan', tx);
this.limitOrphans();
return missing; return missing;
}; };
/** /**
* Resolve orphans and attempt to add to mempool. * Resolve orphans and attempt to add to mempool.
* @method * @method
* @param {TX} tx * @param {TX} parent
* @returns {Promise} - Returns {@link TX}[]. * @returns {Promise} - Returns {@link TX}[].
*/ */
Mempool.prototype.handleOrphans = co(function* handleOrphans(tx) { Mempool.prototype.handleOrphans = co(function* handleOrphans(parent) {
var resolved = this.resolveOrphans(tx); var resolved = this.resolveOrphans(parent);
var i, orphan; var i, orphan, tx, missing;
for (i = 0; i < resolved.length; i++) { for (i = 0; i < resolved.length; i++) {
orphan = resolved[i]; orphan = resolved[i];
try { try {
yield this.insertTX(orphan); tx = orphan.toTX();
} catch (e) {
this.logger.warning('%s %s',
'Warning: possible memory corruption.',
'Orphan failed deserialization.');
}
try {
missing = yield this.insertTX(tx, -1);
} catch (err) { } catch (err) {
if (err.type === 'VerifyError') { if (err.type === 'VerifyError') {
this.logger.debug( this.logger.debug(
'Could not resolve orphan %s: %s.', 'Could not resolve orphan %s: %s.',
orphan.txid(), err.message); tx.txid(), err.message);
if (!orphan.hasWitness() && !err.malleated) if (!tx.hasWitness() && !err.malleated)
this.rejects.add(orphan.hash()); this.rejects.add(tx.hash());
this.emit('bad orphan', err, orphan.id);
continue; continue;
} }
throw err; throw err;
} }
this.logger.debug( assert(!missing);
'Resolved orphan %s in mempool (txs=%d).',
orphan.txid(), this.totalTX); this.logger.debug('Resolved orphan %s in mempool.', tx.txid());
} }
return resolved; return resolved;
@ -1583,37 +1588,33 @@ Mempool.prototype.handleOrphans = co(function* handleOrphans(tx) {
* Potentially resolve any transactions * Potentially resolve any transactions
* that redeem the passed-in transaction. * that redeem the passed-in transaction.
* Deletes all orphan entries and * Deletes all orphan entries and
* returns orphan hashes. * returns orphan objects.
* @param {TX} tx * @param {TX} parent
* @returns {TX[]} Resolved * @returns {Orphan[]}
*/ */
Mempool.prototype.resolveOrphans = function resolveOrphans(tx) { Mempool.prototype.resolveOrphans = function resolveOrphans(parent) {
var hash = tx.hash('hex'); var hash = parent.hash('hex');
var map = this.waiting[hash];
var resolved = []; var resolved = [];
var hashes = this.waiting[hash]; var i, hashes, orphanHash, orphan;
var i, orphanHash, orphan;
if (!hashes) if (!map)
return resolved; return resolved;
hashes = map.keys();
assert(hashes.length > 0);
for (i = 0; i < hashes.length; i++) { for (i = 0; i < hashes.length; i++) {
orphanHash = hashes[i]; orphanHash = hashes[i];
orphan = this.getOrphan(orphanHash); orphan = this.getOrphan(orphanHash);
if (!orphan) assert(orphan);
continue;
if (--orphan.missing === 0) { if (--orphan.missing === 0) {
delete this.orphans[orphanHash]; delete this.orphans[orphanHash];
this.totalOrphans--; this.totalOrphans--;
try { resolved.push(orphan);
resolved.push(orphan.toTX());
} catch (e) {
this.logger.warning('%s %s',
'Warning: possible memory corruption.',
'Orphan failed deserialization.');
}
} }
} }
@ -1625,14 +1626,15 @@ Mempool.prototype.resolveOrphans = function resolveOrphans(tx) {
/** /**
* Remove a transaction from the mempool. * Remove a transaction from the mempool.
* @param {Hash} tx * @param {Hash} tx
* @returns {Boolean}
*/ */
Mempool.prototype.removeOrphan = function removeOrphan(hash) { Mempool.prototype.removeOrphan = function removeOrphan(hash) {
var orphan = this.getOrphan(hash); var orphan = this.getOrphan(hash);
var i, j, tx, hashes, prevout, prev; var i, tx, map, prevout, prev;
if (!orphan) if (!orphan)
return; return false;
try { try {
tx = orphan.toTX(); tx = orphan.toTX();
@ -1649,19 +1651,16 @@ Mempool.prototype.removeOrphan = function removeOrphan(hash) {
for (i = 0; i < prevout.length; i++) { for (i = 0; i < prevout.length; i++) {
prev = prevout[i]; prev = prevout[i];
hashes = this.waiting[prev]; map = this.waiting[prev];
if (!hashes) if (!map)
continue; continue;
j = hashes.indexOf(hash); assert(map.has(hash));
if (j === -1) map.remove(hash);
continue;
hashes.splice(j, 1); if (map.size === 0)
if (hashes.length === 0)
delete this.waiting[prev]; delete this.waiting[prev];
} }
@ -1669,6 +1668,30 @@ Mempool.prototype.removeOrphan = function removeOrphan(hash) {
this.totalOrphans--; this.totalOrphans--;
this.emit('remove orphan', tx); this.emit('remove orphan', tx);
return true;
};
/**
* Remove a random orphan transaction from the mempool.
* @returns {Boolean}
*/
Mempool.prototype.limitOrphans = function limitOrphans() {
var hashes = Object.keys(this.orphans);
var index, hash;
if (this.totalOrphans < this.options.maxOrphans)
return false;
index = crypto.randomRange(0, hashes.length);
hash = hashes[index];
this.logger.debug('Removing orphan %s from mempool.', util.revHex(hash));
this.removeOrphan(hash);
return true;
}; };
/** /**
@ -2059,7 +2082,7 @@ MempoolOptions.prototype.fromOptions = function fromOptions(options) {
} }
if (options.limitFreeRelay != null) { if (options.limitFreeRelay != null) {
assert(util.isNumber(options.limitFreeRelay)); assert(util.isUInt32(options.limitFreeRelay));
this.limitFreeRelay = options.limitFreeRelay; this.limitFreeRelay = options.limitFreeRelay;
} }
@ -2094,27 +2117,27 @@ MempoolOptions.prototype.fromOptions = function fromOptions(options) {
} }
if (options.maxSize != null) { if (options.maxSize != null) {
assert(util.isNumber(options.maxSize)); assert(util.isUInt53(options.maxSize));
this.maxSize = options.maxSize; this.maxSize = options.maxSize;
} }
if (options.maxOrphans != null) { if (options.maxOrphans != null) {
assert(util.isNumber(options.maxOrphans)); assert(util.isUInt32(options.maxOrphans));
this.maxOrphans = options.maxOrphans; this.maxOrphans = options.maxOrphans;
} }
if (options.maxAncestors != null) { if (options.maxAncestors != null) {
assert(util.isNumber(options.maxAncestors)); assert(util.isUInt32(options.maxAncestors));
this.maxAncestors = options.maxAncestors; this.maxAncestors = options.maxAncestors;
} }
if (options.expiryTime != null) { if (options.expiryTime != null) {
assert(util.isNumber(options.expiryTime)); assert(util.isUInt32(options.expiryTime));
this.expiryTime = options.expiryTime; this.expiryTime = options.expiryTime;
} }
if (options.minRelay != null) { if (options.minRelay != null) {
assert(util.isNumber(options.minRelay)); assert(util.isUint53(options.minRelay));
this.minRelay = options.minRelay; this.minRelay = options.minRelay;
} }
@ -2135,12 +2158,12 @@ MempoolOptions.prototype.fromOptions = function fromOptions(options) {
} }
if (options.maxFiles != null) { if (options.maxFiles != null) {
assert(util.isNumber(options.maxFiles)); assert(util.isUInt32(options.maxFiles));
this.maxFiles = options.maxFiles; this.maxFiles = options.maxFiles;
} }
if (options.cacheSize != null) { if (options.cacheSize != null) {
assert(util.isNumber(options.cacheSize)); assert(util.isUInt53(options.cacheSize));
this.cacheSize = options.cacheSize; this.cacheSize = options.cacheSize;
} }
@ -2382,11 +2405,13 @@ IndexedCoin.prototype.toCoin = function toCoin() {
* @ignore * @ignore
* @param {TX} tx * @param {TX} tx
* @param {Hash[]} missing * @param {Hash[]} missing
* @param {Number} id
*/ */
function Orphan(tx, missing) { function Orphan(tx, missing, id) {
this.raw = tx.toRaw(); this.raw = tx.toRaw();
this.missing = missing; this.missing = missing;
this.id = id;
} }
Orphan.prototype.toTX = function toTX() { Orphan.prototype.toTX = function toTX() {

View File

@ -1674,10 +1674,11 @@ RejectPacket.fromRaw = function fromRaw(data, enc) {
* @private * @private
* @param {Number} code * @param {Number} code
* @param {String} reason * @param {String} reason
* @param {(TX|Block)?} msg * @param {String?} msg
* @param {Hash?} hash
*/ */
RejectPacket.prototype.fromReason = function fromReason(code, reason, msg) { RejectPacket.prototype.fromReason = function fromReason(code, reason, msg, hash) {
if (typeof code === 'string') if (typeof code === 'string')
code = RejectPacket.codes[code.toUpperCase()]; code = RejectPacket.codes[code.toUpperCase()];
@ -1692,8 +1693,9 @@ RejectPacket.prototype.fromReason = function fromReason(code, reason, msg) {
this.reason = reason; this.reason = reason;
if (msg) { if (msg) {
this.message = (msg instanceof TX) ? 'tx' : 'block'; assert(hash);
this.hash = msg.hash('hex'); this.message = msg;
this.hash = hash;
} }
return this; return this;
@ -1703,12 +1705,13 @@ RejectPacket.prototype.fromReason = function fromReason(code, reason, msg) {
* Instantiate reject packet from reason message. * Instantiate reject packet from reason message.
* @param {Number} code * @param {Number} code
* @param {String} reason * @param {String} reason
* @param {(TX|Block)?} obj * @param {String?} msg
* @param {Hash?} hash
* @returns {RejectPacket} * @returns {RejectPacket}
*/ */
RejectPacket.fromReason = function fromReason(code, reason, obj) { RejectPacket.fromReason = function fromReason(code, reason, msg, hash) {
return new RejectPacket().fromReason(code, reason, obj); return new RejectPacket().fromReason(code, reason, msg, hash);
}; };
/** /**

View File

@ -79,7 +79,7 @@ function Peer(options) {
this.parser = new Parser(this.network); this.parser = new Parser(this.network);
this.framer = new Framer(this.network); this.framer = new Framer(this.network);
this.id = Peer.uid++; this.id = -1;
this.socket = null; this.socket = null;
this.opened = false; this.opened = false;
this.outbound = false; this.outbound = false;
@ -149,13 +149,6 @@ function Peer(options) {
util.inherits(Peer, EventEmitter); util.inherits(Peer, EventEmitter);
/**
* Peer ID counter.
* @type {Number}
*/
Peer.uid = 0;
/** /**
* Max output bytes buffered before * Max output bytes buffered before
* invoking stall behavior for peer. * invoking stall behavior for peer.
@ -2151,12 +2144,12 @@ Peer.prototype.sendMempool = function sendMempool() {
* @param {TX|Block} msg * @param {TX|Block} msg
*/ */
Peer.prototype.sendReject = function sendReject(code, reason, msg) { Peer.prototype.sendReject = function sendReject(code, reason, msg, hash) {
var reject = packets.RejectPacket.fromReason(code, reason, msg); var reject = packets.RejectPacket.fromReason(code, reason, msg, hash);
if (msg) { if (msg) {
this.logger.debug('Rejecting %s %s (%s): code=%s reason=%s.', this.logger.debug('Rejecting %s %s (%s): code=%s reason=%s.',
reject.message, msg.rhash(), this.hostname(), code, reason); msg, util.revHex(hash), this.hostname(), code, reason);
} else { } else {
this.logger.debug('Rejecting packet from %s: code=%s reason=%s.', this.logger.debug('Rejecting packet from %s: code=%s reason=%s.',
this.hostname(), code, reason); this.hostname(), code, reason);
@ -2223,16 +2216,14 @@ Peer.prototype.ban = function ban() {
/** /**
* Send a `reject` packet to peer. * Send a `reject` packet to peer.
* @see Framer.reject * @see Framer.reject
* @param {(TX|Block)?} msg * @param {String msg
* @param {String} code * @param {VerifyError} err
* @param {String} reason
* @param {Number} score
* @returns {Boolean} * @returns {Boolean}
*/ */
Peer.prototype.reject = function reject(msg, code, reason, score) { Peer.prototype.reject = function reject(msg, err) {
this.sendReject(code, reason, msg); this.sendReject(err.code, err.reason, msg, err.hash);
return this.increaseBan(score); return this.increaseBan(err.score);
}; };
/** /**

View File

@ -109,6 +109,7 @@ function Pool(options) {
this.peers = new PeerList(); this.peers = new PeerList();
this.authdb = new BIP150.AuthDB(this.options); this.authdb = new BIP150.AuthDB(this.options);
this.hosts = new HostList(this.options); this.hosts = new HostList(this.options);
this.id = 0;
if (this.options.spv) if (this.options.spv)
this.spvFilter = Bloom.fromRate(20000, 0.001, Bloom.flags.ALL); this.spvFilter = Bloom.fromRate(20000, 0.001, Bloom.flags.ALL);
@ -190,6 +191,10 @@ Pool.prototype._init = function _init() {
this.mempool.on('tx', function(tx) { this.mempool.on('tx', function(tx) {
self.announceTX(tx); self.announceTX(tx);
}); });
this.mempool.on('bad orphan', function(err, id) {
self.handleBadOrphan('tx', err, id);
});
} }
// Normally we would also broadcast // Normally we would also broadcast
@ -203,6 +208,10 @@ Pool.prototype._init = function _init() {
return; return;
self.announceBlock(block); self.announceBlock(block);
}); });
this.chain.on('bad orphan', function(err, id) {
self.handleBadOrphan('block', err, id);
});
} }
}; };
@ -1162,6 +1171,29 @@ Pool.prototype.createInbound = function createInbound(socket) {
return peer; return peer;
}; };
/**
* Allocate new peer id.
* @returns {Number}
*/
Pool.prototype.uid = function uid() {
var MAX = util.MAX_SAFE_INTEGER;
if (this.id >= MAX - this.peers.size() - 1)
this.id = 0;
// Once we overflow, there's a chance
// of collisions. Unlikely to happen
// unless we have tried to connect 9
// quadrillion times, but still
// account for it.
do {
this.id += 1;
} while (this.peers.find(this.id));
return this.id;
};
/** /**
* Bind to peer events. * Bind to peer events.
* @private * @private
@ -1171,6 +1203,8 @@ Pool.prototype.createInbound = function createInbound(socket) {
Pool.prototype.bindPeer = function bindPeer(peer) { Pool.prototype.bindPeer = function bindPeer(peer) {
var self = this; var self = this;
peer.id = this.uid();
peer.onPacket = function onPacket(packet) { peer.onPacket = function onPacket(packet) {
return self.handlePacket(peer, packet); return self.handlePacket(peer, packet);
}; };
@ -2265,10 +2299,10 @@ Pool.prototype._addBlock = co(function* addBlock(peer, block, flags) {
peer.blockTime = util.ms(); peer.blockTime = util.ms();
try { try {
entry = yield this.chain.add(block, flags); entry = yield this.chain.add(block, flags, peer.id);
} catch (err) { } catch (err) {
if (err.type === 'VerifyError') { if (err.type === 'VerifyError') {
peer.reject(block, err.code, err.reason, err.score); peer.reject('block', err);
this.logger.warning(err); this.logger.warning(err);
return; return;
} }
@ -2395,6 +2429,33 @@ Pool.prototype.switchSync = co(function* switchSync(peer, hash) {
yield this.getBlocks(peer, hash); yield this.getBlocks(peer, hash);
}); });
/**
* Handle bad orphan.
* @method
* @private
* @param {String} msg
* @param {VerifyError} err
* @param {Number} id
*/
Pool.prototype.handleBadOrphan = function handleBadOrphan(msg, err, id) {
var peer = this.peers.find(id);
if (!peer) {
this.logger.warning(
'Could not find offending peer for orphan: %s (%d).',
util.revHex(err.hash), id);
return;
}
this.logger.debug(
'Punishing peer for sending a bad orphan (%s).',
peer.hostname());
// Punish the original peer who sent this.
peer.reject(msg, err);
};
/** /**
* Log sync status. * Log sync status.
* @private * @private
@ -2402,23 +2463,21 @@ Pool.prototype.switchSync = co(function* switchSync(peer, hash) {
*/ */
Pool.prototype.logStatus = function logStatus(block) { Pool.prototype.logStatus = function logStatus(block) {
if (this.chain.total % 20 === 0) { if (this.chain.height % 20 === 0) {
this.logger.debug('Status:' this.logger.debug('Status:'
+ ' ts=%s height=%d progress=%s' + ' ts=%s height=%d progress=%s'
+ ' blocks=%d orphans=%d active=%d' + ' orphans=%d active=%d'
+ ' target=%s peers=%d jobs=%d', + ' target=%s peers=%d',
util.date(block.ts), util.date(block.ts),
this.chain.height, this.chain.height,
(this.chain.getProgress() * 100).toFixed(2) + '%', (this.chain.getProgress() * 100).toFixed(2) + '%',
this.chain.total,
this.chain.orphanCount, this.chain.orphanCount,
this.blockMap.size, this.blockMap.size,
block.bits, block.bits,
this.peers.size(), this.peers.size());
this.locker.jobs.length);
} }
if (this.chain.total % 2000 === 0) { if (this.chain.height % 2000 === 0) {
this.logger.info( this.logger.info(
'Received 2000 more blocks (height=%d, hash=%s).', 'Received 2000 more blocks (height=%d, hash=%s).',
this.chain.height, this.chain.height,
@ -2504,10 +2563,10 @@ Pool.prototype._handleTX = co(function* handleTX(peer, packet) {
} }
try { try {
missing = yield this.mempool.addTX(tx); missing = yield this.mempool.addTX(tx, peer.id);
} catch (err) { } catch (err) {
if (err.type === 'VerifyError') { if (err.type === 'VerifyError') {
peer.reject(tx, err.code, err.reason, err.score); peer.reject('tx', err);
this.logger.info(err); this.logger.info(err);
return; return;
} }
@ -4164,6 +4223,7 @@ PoolOptions.prototype._resolve = function resolve(name) {
function PeerList() { function PeerList() {
this.map = {}; this.map = {};
this.ids = {};
this.list = new List(); this.list = new List();
this.load = null; this.load = null;
this.inbound = 0; this.inbound = 0;
@ -4208,6 +4268,9 @@ PeerList.prototype.add = function add(peer) {
assert(!this.map[peer.hostname()]); assert(!this.map[peer.hostname()]);
this.map[peer.hostname()] = peer; this.map[peer.hostname()] = peer;
assert(!this.ids[peer.id]);
this.ids[peer.id] = peer;
if (peer.outbound) if (peer.outbound)
this.outbound++; this.outbound++;
else else
@ -4222,6 +4285,9 @@ PeerList.prototype.add = function add(peer) {
PeerList.prototype.remove = function remove(peer) { PeerList.prototype.remove = function remove(peer) {
assert(this.list.remove(peer)); assert(this.list.remove(peer));
assert(this.ids[peer.id]);
delete this.ids[peer.id];
assert(this.map[peer.hostname()]); assert(this.map[peer.hostname()]);
delete this.map[peer.hostname()]; delete this.map[peer.hostname()];
@ -4257,6 +4323,16 @@ PeerList.prototype.has = function has(hostname) {
return this.map[hostname] != null; return this.map[hostname] != null;
}; };
/**
* Get peer by ID.
* @param {Number} id
* @returns {Peer}
*/
PeerList.prototype.find = function find(id) {
return this.ids[id];
};
/** /**
* Destroy peer list (kills peers). * Destroy peer list (kills peers).
*/ */

View File

@ -50,6 +50,7 @@ function VerifyError(msg, code, reason, score, malleated) {
this.code = code; this.code = code;
this.reason = reason; this.reason = reason;
this.score = score; this.score = score;
this.hash = msg.hash('hex');
this.malleated = malleated || false; this.malleated = malleated || false;
this.message = 'Verification failure: ' + reason this.message = 'Verification failure: ' + reason

View File

@ -7,8 +7,6 @@
'use strict'; 'use strict';
/* global gc */
var assert = require('assert'); var assert = require('assert');
var nodeUtil = require('util'); var nodeUtil = require('util');
var os = require('os'); var os = require('os');
@ -83,15 +81,6 @@ if (os.homedir) {
util.nop = function() {}; util.nop = function() {};
/**
* Garbage collector for `--expose-gc`.
* @type function
* @static
* @method
*/
util.gc = !util.isBrowser && typeof gc === 'function' ? gc : util.nop;
/** /**
* Clone a buffer. * Clone a buffer.
* @param {Buffer} data * @param {Buffer} data