diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 6bf25d00..bae5f90c 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -13,6 +13,7 @@ var network = bcoin.protocol.network; var utils = require('./utils'); var assert = utils.assert; var BufferReader = require('./reader'); +var VerifyError = utils.VerifyError; /** * Chain @@ -61,15 +62,27 @@ utils.inherits(Chain, EventEmitter); Chain.prototype._init = function _init() { var self = this; + function getPeer() { + if (!self.node || !self.node.pool) + return; + + return self.node.pool.peers.load; + } + + function getHost() { + var peer = getPeer(); + if (peer) + return peer.host; + return 'unknown'; + } + // Hook into events for debugging - // this.on('block', function(block, entry, peer) { - // var host = peer ? peer.host : 'unknown'; + // this.on('block', function(block, entry) { // utils.debug('Block %s (%d) added to chain (%s)', - // utils.revHex(entry.hash), entry.height, host); + // utils.revHex(entry.hash), entry.height, getHost()); // }); - this.on('competitor', function(block, entry, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('competitor', function(block, entry) { utils.debug('Heads up: Competing chain at height %d:' + ' tip-height=%d competitor-height=%d' + ' tip-hash=%s competitor-hash=%s' @@ -83,64 +96,58 @@ Chain.prototype._init = function _init() { self.tip.chainwork.toString(), entry.chainwork.toString(), self.tip.chainwork.sub(entry.chainwork).toString(), - host); + getHost()); }); - this.on('resolved', function(block, entry, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('resolved', function(block, entry) { utils.debug('Orphan %s (%d) was resolved (%s)', - utils.revHex(entry.hash), entry.height, host); + utils.revHex(entry.hash), entry.height, getHost()); }); - this.on('checkpoint', function(block, data, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('checkpoint', function(block, data) { utils.debug('Hit checkpoint block %s (%d) (%s)', - utils.revHex(data.checkpoint), data.height, host); + utils.revHex(data.checkpoint), data.height, getHost()); }); - this.on('fork', function(block, data, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('fork', function(block, data) { utils.debug( - 'Fork at height %d: expected=%s received=%s checkpoint=%s peer=%s', + 'Fork at height %d: expected=%s received=%s checkpoint=%s', data.height, utils.revHex(data.expected), utils.revHex(data.received), data.checkpoint, - host + getHost() ); if (data.checkpoint) utils.debug('WARNING: Block failed a checkpoint.'); }); - this.on('invalid', function(block, data, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('invalid', function(block, data) { utils.debug( - 'Invalid block at height %d: hash=%s peer=%s', + 'Invalid block at height %d: hash=%s', data.height, utils.revHex(data.hash), - host + getHost() ); if (data.chain) { utils.debug( 'Peer is sending an invalid continuation chain (%s)', - host); + getHost()); } else if (data.seen) { - utils.debug('Peer is sending an invalid chain (%s)', host); + utils.debug('Peer is sending an invalid chain (%s)', getHost()); } }); - this.on('exists', function(block, data, peer) { - var host = peer ? peer.host : 'unknown'; + this.on('exists', function(block, data) { utils.debug('Already have block %s (%s)', - data.height, host); + data.height, getHost()); }); - this.on('orphan', function(block, data, peer) { - var host = peer ? peer.host : 'unknown'; - utils.debug('Handled orphan %s (%s)', utils.revHex(data.hash), host); + this.on('orphan', function(block, data) { + utils.debug('Handled orphan %s (%s)', utils.revHex(data.hash), getHost()); }); - this.on('purge', function(count, size, peer) { + this.on('purge', function(count, size) { utils.debug('Warning: %d (%dmb) orphans cleared!', count, utils.mb(size)); }); @@ -366,37 +373,28 @@ Chain.prototype._preload = function _preload(callback) { }); }; -Chain.prototype._verifyContext = function _verifyContext(block, prev, peer, callback) { +Chain.prototype._verifyContext = function _verifyContext(block, prev, callback) { var self = this; - this._verify(block, prev, peer, function(err, flags) { + this._verify(block, prev, function(err, flags) { if (err) return callback(err); - if (flags === false) - return callback(null, false); - - self._checkDuplicates(block, prev, peer, function(err, result) { + self._checkDuplicates(block, prev, function(err, result) { if (err) return callback(err); - if (!result) - return callback(null, false); - - self._checkInputs(block, prev, flags, peer, function(err, result) { + self._checkInputs(block, prev, flags, function(err) { if (err) return callback(err); - if (!result) - return callback(null, false); - - return callback(null, true); + return callback(); }); }); }); }; -Chain.prototype._verify = function _verify(block, prev, peer, callback) { +Chain.prototype._verify = function _verify(block, prev, callback) { var self = this; var flags = constants.flags.MANDATORY_VERIFY_FLAGS; var height, ts, i, tx, coinbaseHeight; @@ -408,11 +406,8 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { callback(err, result); } - if (!block.verify(ret)) { - self.emit('verify-error', - block, 'invalid', ret.reason, ret.score, peer); - return done(null, false); - } + if (!block.verify(ret)) + return done(new VerifyError(block, 'invalid', ret.reason, ret.score)); // Skip the genesis block if (block.isGenesis()) @@ -421,12 +416,12 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // Ensure it's not an orphan if (!prev) { utils.debug('Block has no previous entry: %s', block.rhash); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'bad-prevblk', 0)); } prev.ensureAncestors(function(err) { if (err) - return callback(err); + return done(err); height = prev.height + 1; medianTime = prev.getMedianTime(); @@ -434,16 +429,12 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // Ensure the timestamp is correct if (block.ts <= medianTime) { utils.debug('Block time is lower than median: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'time-too-old', 0, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'time-too-old', 0)); } if (block.bits !== self.getTarget(prev, block)) { utils.debug('Block is using wrong target: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'bad-diffbits', 100, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'bad-diffbits', 100)); } // For some reason bitcoind has p2sh in the @@ -461,27 +452,21 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // once the majority of blocks are using it. if (block.version < 2 && prev.isOutdated(2)) { utils.debug('Block is outdated (v2): %s', block.rhash); - self.emit('verify-error', - block, 'obsolete', 'bad-version', 0, peer); - return done(null, false); + return done(new VerifyError(block, 'obsolete', 'bad-version', 0)); } // Only allow version 3 blocks (sig validation) // once the majority of blocks are using it. if (block.version < 3 && prev.isOutdated(3)) { utils.debug('Block is outdated (v3): %s', block.rhash); - self.emit('verify-error', - block, 'obsolete', 'bad-version', 0, peer); - return done(null, false); + return done(new VerifyError(block, 'obsolete', 'bad-version', 0)); } // Only allow version 4 blocks (checklocktimeverify) // once the majority of blocks are using it. if (block.version < 4 && prev.isOutdated(4)) { utils.debug('Block is outdated (v4): %s', block.rhash); - self.emit('verify-error', - block, 'obsolete', 'bad-version', 0, peer); - return done(null, false); + return done(new VerifyError(block, 'obsolete', 'bad-version', 0)); } // Only allow version 5 blocks (segwit) @@ -489,9 +474,7 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { if (network.segwitHeight !== -1 && height >= network.segwitHeight) { if (block.version < 5 && prev.isOutdated(5)) { utils.debug('Block is outdated (v5): %s', block.rhash); - self.emit('verify-error', - block, 'obsolete', 'bad-version', 0, peer); - return done(null, false); + return done(new VerifyError(block, 'obsolete', 'bad-version', 0)); } } @@ -535,25 +518,19 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { if (coinbaseHeight) { if (block.getCoinbaseHeight() !== height) { utils.debug('Block has bad coinbase height: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'bad-cb-height', 100, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'bad-cb-height', 100)); } } if (block.version >= 5 && segwit) { if (block.commitmentHash !== block.getCommitmentHash()) { utils.debug('Block failed witnessroot test: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'bad-blk-wit-length', 100, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'bad-blk-wit-length', 100)); } } else { if (block.hasWitness()) { utils.debug('Unexpected witness data found: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'unexpected-witness', 100, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'unexpected-witness', 100)); } } @@ -568,9 +545,7 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // regards to nSequence and nLockTime. if (!tx.isFinal(height, ts)) { utils.debug('TX is not final: %s (%s)', block.rhash, i); - self.emit('verify-error', - block, 'invalid', 'bad-txns-nonfinal', 10, peer); - return done(null, false); + return done(new VerifyError(block, 'invalid', 'bad-txns-nonfinal', 10)); } } @@ -578,18 +553,18 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { }); }; -Chain.prototype._checkDuplicates = function _checkDuplicates(block, prev, peer, callback) { +Chain.prototype._checkDuplicates = function _checkDuplicates(block, prev, callback) { var self = this; var height = prev.height + 1; if (this.options.spv || block.type !== 'block') - return callback(null, true); + return callback(); if (block.isGenesis()) - return callback(null, true); + return callback(); // Check all transactions - utils.everySerial(block.txs, function(tx, next) { + utils.forEachSerial(block.txs, function(tx, next) { var hash = tx.hash('hex'); // BIP30 - Ensure there are no duplicate txids @@ -603,41 +578,45 @@ Chain.prototype._checkDuplicates = function _checkDuplicates(block, prev, peer, if (result) { utils.debug('Block is overwriting txids: %s', block.rhash); if (network.type === 'main' && (height === 91842 || height === 91880)) - return next(null, true); - self.emit('verify-error', - block, 'invalid', 'bad-txns-BIP30', 100, peer); - return next(null, false); + return next(); + return next(new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100)); } - next(null, true); + next(); }); }, callback); }; -Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, callback) { +Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callback) { var height = prev.height + 1; var scriptCheck = true; + var historical = false; if (this.options.spv || block.type !== 'block') - return callback(null, true); + return callback(); if (block.isGenesis()) - return callback(null, true); + return callback(); // If we are an ancestor of a checkpoint, we can // skip the input verification. - if (this.options.useCheckpoints) { - if (height < network.checkpoints.lastHeight && !network.checkpoints[height]) + if (height <= network.checkpoints.lastHeight) { + if (this.options.useCheckpoints) scriptCheck = false; + historical = true; } this.db.fillBlock(block, function(err) { - var i, j, input, tx, hash; + var ret = {}; var sigops = 0; + var i, j, input, tx, hash; if (err) return callback(err); + if (!self._checkReward(block)) + return callback(new VerifyError(block, 'invalid', 'bad-cb-amount', 100)); + // Check all transactions for (i = 0; i < block.txs.length; i++) { tx = block.txs[i]; @@ -653,19 +632,15 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, c if (sigops > constants.block.maxSigops) { utils.debug('Block has too many sigops: %s', block.rhash); - self.emit('verify-error', - block, 'invalid', 'bad-blk-sigops', 100, peer); - return callback(null, false); + return callback(new VerifyError(block, 'invalid', 'bad-blk-sigops', 100)); } // Coinbases do not have prevouts if (tx.isCoinbase()) continue; - if (tx.getOutputValue().cmp(tx.getInputValue()) > 0) { - utils.debug('TX is spending funds it does not have: %s', tx.rhash); - return false; - } + if (!tx.checkInputs(height, ret)) + return callback(new VerifyError(block, 'invalid', ret.reason, ret.score)); for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; @@ -675,19 +650,19 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, c utils.debug('Block is using spent inputs: %s (tx: %s, output: %s)', block.rhash, tx.rhash, utils.revHex(input.prevout.hash) + '/' + input.prevout.index); - if (height < network.checkpoints.lastHeight) - throw new Error('BUG: Spent inputs in historical data!'); - self.emit('verify-error', - block, 'invalid', 'bad-txns-inputs-missingorspent', 100, peer); - return callback(null, false); + assert(!historical, 'BUG: Spent inputs in historical data!'); + return callback(new VerifyError(block, + 'invalid', + 'bad-txns-inputs-missingorspent', + 100)); } + if (self.options.verifySync !== true) + continue; + if (!scriptCheck) continue; - // Disable. Use workers and verifyAsync for now. - continue; - // Verify the scripts if (!tx.verify(j, true, flags)) { utils.debug('Block has invalid inputs: %s (%s/%d)', @@ -701,23 +676,39 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, c utils.toHex(input.coin.script.encode())); utils.debug('Reserialized Script: %s', utils.toHex(input.coin.script.clone().encode())); - if (height < network.checkpoints.lastHeight) - throw new Error('BUG: Bad inputs in historical data!'); - return callback(null, false); + assert(!historical, 'BUG: Invalid inputs in historical data!'); + return callback(new VerifyError(block, + 'invalid', + 'mandatory-script-verify-flag-failed', + 100)); } } } - // Disable. Use workers and verifyAsync for now. - // return callback(null, true); + if (self.options.verifySync === true) + return callback(); if (!scriptCheck) - return callback(null, true); + return callback(); // Verify all txs in parallel. utils.every(block.txs, function(tx, next) { tx.verifyAsync(null, true, flags, next); - }, callback); + }, function(err, verified) { + if (err) + return callback(err); + + if (!verified) { + utils.debug('Block has invalid inputs: %s', block.rhash); + assert(!historical, 'BUG: Invalid inputs in historical data!'); + return callback(new VerifyError(block, + 'invalid', + 'mandatory-script-verify-flag-failed', + 100)); + } + + return callback(); + }); }); }; @@ -730,13 +721,7 @@ Chain.prototype._checkReward = function _checkReward(block) { for (i = 1; i < block.txs.length; i++) actual.iadd(block.txs[i].getFee()); - if (claimed.cmp(actual) > 0) { - self.emit('verify-error', - block, 'invalid', 'bad-cb-amount', 100, peer); - return false; - } - - return true; + return claimed.cmp(actual) <= 0; }; Chain.prototype.getHeight = function getHeight(hash) { @@ -1001,14 +986,14 @@ Chain.prototype.onFlush = function onFlush(callback) { return this.locker.onFlush(callback); }; -Chain.prototype.add = function add(block, peer, callback, force) { +Chain.prototype.add = function add(block, callback, force) { var self = this; var total = 0; var ret = {}; assert(this.loaded); - var unlock = this._lock(add, [block, peer, callback], force); + var unlock = this._lock(add, [block, callback], force); if (!unlock) return; @@ -1024,11 +1009,9 @@ Chain.prototype.add = function add(block, peer, callback, force) { hash: hash, seen: !!self.invalid[hash], chain: !!self.invalid[prevHash] - }, peer); + }); self.invalid[hash] = true; - self.emit('verify-error', - block, 'duplicate', 'duplicate', 0, peer); - return done(); + return done(new VerifyError(block, 'duplicate', 'duplicate', 0)); } // Do we already have this block? @@ -1040,8 +1023,8 @@ Chain.prototype.add = function add(block, peer, callback, force) { self.emit('exists', block, { height: -1, hash: hash - }, peer); - return done(); + }); + return done(new VerifyError(block, 'duplicate', 'duplicate', 0)); } // Find the previous block height/index. @@ -1062,10 +1045,8 @@ Chain.prototype.add = function add(block, peer, callback, force) { hash: hash, seen: false, chain: false - }, peer); - self.emit('verify-error', - block, 'invalid', ret.reason, ret.score, peer); - return done(); + }); + return done(new VerifyError(block, 'invalid', ret.reason, ret.score)); } // Special case for genesis block. @@ -1087,7 +1068,7 @@ Chain.prototype.add = function add(block, peer, callback, force) { expected: orphan.hash('hex'), received: hash, checkpoint: false - }, peer); + }); return done(); } @@ -1096,12 +1077,9 @@ Chain.prototype.add = function add(block, peer, callback, force) { height: block.getCoinbaseHeight(), hash: hash, seen: true - }, peer); + }); - self.emit('verify-error', - block, 'invalid', 'bad-prevblk', 0, peer); - - return done(); + return done(new VerifyError(block, 'invalid', 'bad-prevblk', 0)); } // Update the best height based on the coinbase. @@ -1124,10 +1102,8 @@ Chain.prototype.add = function add(block, peer, callback, force) { height: block.getCoinbaseHeight(), hash: hash, seen: false - }, peer); - self.emit('verify-error', - block, 'invalid', 'bad-prevblk', 0, peer); - return done(); + }); + return done(new VerifyError(block, 'invalid', 'bad-prevblk', 0)); } // Verify the checkpoint. @@ -1137,7 +1113,7 @@ Chain.prototype.add = function add(block, peer, callback, force) { height: height, hash: hash, checkpoint: checkpoint - }, peer); + }); // Block did not match the checkpoint. The // chain could be reset to the last sane @@ -1154,12 +1130,12 @@ Chain.prototype.add = function add(block, peer, callback, force) { expected: checkpoint, received: hash, checkpoint: true - }, peer); + }); - self.emit('verify-error', - block, 'checkpoint', 'checkpoint mismatch', 100, peer); - - return done(); + return done(new VerifyError(block, + 'checkpoint', + 'checkpoint mismatch', + 100)); } } @@ -1178,10 +1154,10 @@ Chain.prototype.add = function add(block, peer, callback, force) { try { block = block.toBlock(); } catch (e) { - // Ugly hack to handle - // the error properly. - peer.parser.emit('error', e); - return done(e); + return done(new VerifyError(block, + 'malformed', + 'error parsing message', + 100)); } } @@ -1196,30 +1172,28 @@ Chain.prototype.add = function add(block, peer, callback, force) { // Do "contextual" verification on our block // now that we're certain its previous // block is in the chain. - self._verifyContext(block, prev, peer, function(err, verified) { + self._verifyContext(block, prev, function(err) { var entry; - // Couldn't verify block. - // Revert the height. - if (err || !verified) { + if (err) { + // Couldn't verify block. + // Revert the height. block.height = -1; block.txs.forEach(function(tx) { tx.height = -1; }); - } - if (err) + if (err.type === 'VerifyError') { + self.invalid[hash] = true; + self.emit('invalid', block, { + height: height, + hash: hash, + seen: false, + chain: false + }); + } + return done(err); - - if (!verified) { - self.invalid[hash] = true; - self.emit('invalid', block, { - height: height, - hash: hash, - seen: false, - chain: false - }, peer); - return done(); } // Create a new chain entry. @@ -1246,12 +1220,12 @@ Chain.prototype.add = function add(block, peer, callback, force) { // Emit our block (and potentially resolved // orphan) only if it is on the main chain. if (mainChain) - self.emit('block', block, entry, peer); + self.emit('block', block, entry); else - self.emit('competitor', block, entry, peer); + self.emit('competitor', block, entry); if (!initial) - self.emit('resolved', block, entry, peer); + self.emit('resolved', block, entry); // No orphan chain. if (!self.orphan.map[hash]) @@ -1275,7 +1249,7 @@ Chain.prototype.add = function add(block, peer, callback, force) { // Failsafe for large orphan chains. Do not // allow more than 20mb stored in memory. if (self.orphan.size > self.orphanLimit) - self.pruneOrphans(peer); + self.pruneOrphans(); // Keep track of total blocks handled. self.total += total; @@ -1308,7 +1282,7 @@ Chain.prototype.purgeOrphans = function purgeOrphans() { this.orphan.size = 0; }; -Chain.prototype.pruneOrphans = function pruneOrphans(peer) { +Chain.prototype.pruneOrphans = function pruneOrphans() { var self = this; var best, last; @@ -1336,7 +1310,7 @@ Chain.prototype.pruneOrphans = function pruneOrphans(peer) { Object.keys(this.orphan.bmap).forEach(function(hash) { var orphan = self.orphan.bmap[hash]; if (orphan !== best) - self.emit('unresolved', orphan, peer); + self.emit('unresolved', orphan); }); this.orphan.map = {}; diff --git a/lib/bcoin/locker.js b/lib/bcoin/locker.js index a76e4ddf..58e286f3 100644 --- a/lib/bcoin/locker.js +++ b/lib/bcoin/locker.js @@ -93,6 +93,7 @@ Locker.prototype.lock = function lock(func, args, force) { Locker.prototype.purgePending = function purgePending() { var self = this; + var total = this.pending.length; assert(this.add); @@ -109,6 +110,9 @@ Locker.prototype.purgePending = function purgePending() { this.jobs = this.jobs.filter(function(item) { return item[0] !== self.add; }); + + if (total !== 0) + this.emit('flush'); }; Locker.prototype.onFlush = function onFlush(callback) { diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index ee102227..80d90f57 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -14,7 +14,7 @@ var utils = require('./utils'); var assert = utils.assert; var BufferWriter = require('./writer'); var BufferReader = require('./reader'); -var DUMMY_PEER = { sendReject: function() {} }; +var VerifyError = utils.VerifyError; /** * Mempool @@ -283,24 +283,16 @@ Mempool.prototype.hasTX = function hasTX(hash, callback) { }; Mempool.prototype.add = -Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { +Mempool.prototype.addTX = function addTX(tx, callback, force) { var self = this; var hash, ts, height, now; var flags = Mempool.flags; var ret = {}; - var unlock = this._lock(addTX, [tx, peer, callback], force); + var unlock = this._lock(addTX, [tx, callback], force); if (!unlock) return; - if (typeof peer === 'function') { - callback = peer; - peer = null; - } - - if (!peer) - peer = DUMMY_PEER; - if (this.chain.segwitActive) { flags |= constants.flags.VERIFY_WITNESS; flags |= constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM; @@ -312,29 +304,22 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { callback = utils.asyncify(callback); if (tx.ts !== 0) { - peer.sendReject(tx, 'alreadyknown', 'txn-already-in-mempool', 0); - return callback(new VerifyError( + return callback(new VerifyError(tx, 'alreadyknown', 'txn-already-in-mempool', 0)); } if (!this.chain.segwitActive) { - if (tx.hasWitness()) { - peer.sendReject(tx, 'nonstandard', 'no-witness-yet', 0); - return callback(new VerifyError('nonstandard', 'no-witness-yet', 0)); - } + if (tx.hasWitness()) + return callback(new VerifyError(tx, 'nonstandard', 'no-witness-yet', 0)); } - if (!tx.isSane(ret)) { - peer.sendReject(tx, 'invalid', ret.reason, ret.score); - return callback(new VerifyError('invalid', ret.reason, ret.score)); - } + if (!tx.isSane(ret)) + return callback(new VerifyError(tx, 'invalid', ret.reason, ret.score)); - if (tx.isCoinbase()) { - peer.sendReject(tx, 'invalid', 'coinbase', 100); - return callback(new VerifyError('invalid', 'coinbase', 100)); - } + if (tx.isCoinbase()) + return callback(new VerifyError(tx, 'invalid', 'coinbase', 100)); // ts = locktimeFlags & LOCKTIME_MEDIAN_PAST // ? self.chain.tip.getMedianTime() @@ -343,34 +328,27 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { ts = utils.now(); height = this.chain.height + 1; - if (!tx.isFinal(ts, height)) { - peer.sendReject(tx, 'nonstandard', 'non-final', 0); - return callback(new VerifyError('nonstandard', 'non-final', 0)); - } + if (!tx.isFinal(ts, height)) + return callback(new VerifyError(tx, 'nonstandard', 'non-final', 0)); if (this.requireStandard) { - if (!tx.isStandard(flags, ret)) { - peer.sendReject(tx, 'nonstandard', ret.reason, 0); - return callback(new VerifyError(ret.reason, 0)); - } + if (!tx.isStandard(flags, ret)) + return callback(new VerifyError(tx, ret.reason, 0)); } this._hasTX(tx, function(err, exists) { if (err) return callback(err); - if (exists) { - peer.sendReject(tx, 'alreadyknown', 'txn-already-in-mempool', 0); - return callback(); - } + if (exists) + return callback(new VerifyError(tx, 'alreadyknown', 'txn-already-in-mempool', 0)); self.tx.isDoubleSpend(tx, function(err, doubleSpend) { if (err) return callback(err); if (doubleSpend) { - peer.sendReject(tx, 'duplicate', 'bad-txns-inputs-spent', 0); - return callback(new VerifyError( + return callback(new VerifyError(tx, 'duplicate', 'bad-txns-inputs-spent', 0)); @@ -382,7 +360,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { if (!tx.hasCoins()) { if (self.size > Mempool.MAX_MEMPOOL_SIZE) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'insufficientfee', 'mempool full', 0)); @@ -392,24 +370,21 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { } self.verify(tx, function(err) { - if (err) { - if (err.type === 'VerifyError' && err.score >= 0) - peer.sendReject(tx, err.code, err.reason, err.score); + if (err) return callback(err); - } self.limitMempoolSize(function(err, result) { if (err) return callback(err); if (!result) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'insufficientfee', 'mempool full', 0)); } - self.addUnchecked(tx, peer, callback); + self.addUnchecked(tx, callback); }); }); }); @@ -417,7 +392,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { }); }; -Mempool.prototype.addUnchecked = function addUnchecked(tx, peer, callback) { +Mempool.prototype.addUnchecked = function addUnchecked(tx, callback) { var self = this; this.tx.addUnchecked(tx, function(err) { if (err) @@ -437,7 +412,7 @@ Mempool.prototype.addUnchecked = function addUnchecked(tx, peer, callback) { return callback(err); utils.forEachSerial(resolved, function(tx, next) { - self.addUnchecked(tx, peer, function(err) { + self.addUnchecked(tx, function(err) { if (err) self.emit('error', err); utils.debug('Resolved orphan %s in mempool.', tx.rhash); @@ -462,9 +437,10 @@ Mempool.prototype.removeUnchecked = function removeUnchecked(tx, callback) { Mempool.prototype.verify = function verify(tx, callback) { var self = this; var height = this.chain.height + 1; - var total, input, coin, i, fee, now, free, minFee; var flags = Mempool.flags; var mandatory = Mempool.mandatory; + var ret = {}; + var fee, now, free, minFee; if (this.chain.segwitActive) { flags |= constants.flags.VERIFY_WITNESS; @@ -477,84 +453,42 @@ Mempool.prototype.verify = function verify(tx, callback) { return callback(err); if (!result) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'non-BIP68-final', 0)); } if (self.requireStandard && !tx.hasStandardInputs(flags)) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'bad-txns-nonstandard-inputs', 0)); } if (tx.getSigops(true) > constants.tx.maxSigops) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'bad-txns-too-many-sigops', 0)); } - total = new bn(0); - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - coin = input.coin; - - if (coin.coinbase) { - if (self.chain.height - coin.height < constants.tx.coinbaseMaturity) { - return callback(new VerifyError( - 'invalid', - 'bad-txns-premature-spend-of-coinbase', - 0)); - } - } - - if (coin.value.cmpn(0) < 0 || coin.value.cmp(constants.maxMoney) > 0) { - return callback(new VerifyError( - 'invalid', - 'bad-txns-inputvalues-outofrange', - 100)); - } - - total.iadd(coin.value); - } - - if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) { - return callback(new VerifyError( - 'invalid', - 'bad-txns-inputvalues-outofrange', - 100)); - } - - if (tx.getOutputValue().cmp(total) > 0) - return callback(new VerifyError('invalid', 'bad-txns-in-belowout', 100)); - - fee = total.sub(tx.getOutputValue()); - - if (fee.cmpn(0) < 0) - return callback(new VerifyError('invalid', 'bad-txns-fee-negative', 100)); - - if (fee.cmp(constants.maxMoney) > 0) { - return callback(new VerifyError( - 'invalid', - 'bad-txns-fee-outofrange', - 100)); - } + if (!tx.checkInputs(height, ret)) + return callback(new VerifyError(tx, 'invalid', ret.reason, ret.score)); + fee = tx.getFee(); minFee = tx.getMinFee(); if (fee.cmp(minFee) < 0) { if (self.relayPriority && fee.cmpn(0) === 0) { free = tx.isFree(height); if (!free) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'insufficientfee', 'insufficient priority', 0)); } } else { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'insufficientfee', 'insufficient fee', 0)); @@ -571,7 +505,7 @@ Mempool.prototype.verify = function verify(tx, callback) { self.lastTime = now; if (self.freeCount > self.limitFreeRelay * 10 * 1000) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'insufficientfee', 'rate limited free transaction', 0)); @@ -581,14 +515,14 @@ Mempool.prototype.verify = function verify(tx, callback) { } if (self.rejectInsaneFees && fee.cmp(minFee.muln(10000)) > 0) - return callback(new VerifyError('highfee', 'absurdly-high-fee', 0)); + return callback(new VerifyError(tx, 'highfee', 'absurdly-high-fee', 0)); self.countAncestors(tx, function(err, count) { if (err) return callback(err); if (count > Mempool.ANCESTOR_LIMIT) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'too-long-mempool-chain', 0)); @@ -605,13 +539,13 @@ Mempool.prototype.verify = function verify(tx, callback) { return callback(err); if (result) { - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'non-mandatory-script-verify-flag', 0)); } - return callback(new VerifyError( + return callback(new VerifyError(tx, 'nonstandard', 'mandatory-script-verify-flag', 0)); @@ -920,23 +854,6 @@ Mempool.prototype.checkMempoolLocks = function checkMempoolLocks(tx, flags, call this.checkLocks(tx, flags, index, callback); }; -/** - * VerifyError - */ - -function VerifyError(code, reason, score) { - Error.call(this); - if (Error.captureStackTrace) - Error.captureStackTrace(this, VerifyError); - this.type = 'VerifyError'; - this.code = code; - this.message = reason; - this.reason = score === -1 ? null : reason; - this.score = score; -} - -utils.inherits(VerifyError, Error); - /** * Expose */ diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 00cc98df..d1cc5687 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -193,10 +193,6 @@ Pool.prototype._init = function _init() { } }); - this.chain.on('verify-error', function(block, code, reason, score, peer) { - peer.sendReject(block, code, reason, score); - }); - this.chain.on('fork', function(block, data, peer) { self.emit('fork', data, peer); }); @@ -448,14 +444,12 @@ Pool.prototype._addLoader = function _addLoader() { // If the peer sent us a block that was added // to the chain (not orphans), reset the timeout. - self._handleBlock(block, peer, function(err, added) { + self._handleBlock(block, peer, function(err) { if (err) - self.emit('error', err); + return self.emit('error', err); - if (added) { - self._startInterval(); - self._startTimer(); - } + self._startInterval(); + self._startTimer(); }); }); @@ -468,14 +462,12 @@ Pool.prototype._addLoader = function _addLoader() { // If the peer sent us a block that was added // to the chain (not orphans), reset the timeout. - self._handleBlock(block, peer, function(err, added) { + self._handleBlock(block, peer, function(err) { if (err) - self.emit('error', err); + return self.emit('error', err); - if (added) { - self._startInterval(); - self._startTimer(); - } + self._startInterval(); + self._startTimer(); }); }); @@ -713,22 +705,33 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { utils.debug( 'Recieved unrequested block: %s (%s)', block.rhash, peer.host); - return callback(null, false); + return callback(); } this._prehandleBlock(block, peer, function(err) { if (err) return callback(err); - self.chain.add(block, peer, function(err, added) { - if (err) + self.chain.add(block, function(err) { + if (err) { + if (err.type === 'VerifyError') { + if (err.score >= 0) + peer.sendReject(block, err.code, err.reason, err.score); + + if (err.reason === 'bad-prevblk') { + // self.chain.purgePending(); + // self.resolveOrphan(peer, null, block.hash('hex')); + } + + self.scheduleRequests(peer); + + return callback(err); + } return callback(err); + } self.scheduleRequests(peer); - if (added === 0) - return callback(null, false); - self.emit('chain-progress', self.chain.getProgress(), peer); if (self.chain.total % 20 === 0) { @@ -749,7 +752,7 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { self.chain.locker.jobs.length); } - return callback(null, true); + return callback(); }); }); }; @@ -828,7 +831,10 @@ Pool.prototype._createPeer = function _createPeer(options) { }); peer.on('tx', function(tx) { - self._handleTX(tx, peer); + self._handleTX(tx, peer, function(err) { + if (err) + self.emit('error', err); + }); }); peer.on('addr', function(data) { @@ -905,8 +911,13 @@ Pool.prototype._handleTX = function _handleTX(tx, peer, callback) { return callback(err); addMempool(tx, peer, function(err) { - if (err) - utils.debug('Mempool error: %s', err.message); + if (err) { + if (err.type === 'VerifyError') { + if (err.score >= 0) + peer.sendReject(block, err.code, err.reason, err.score); + return callback(); + } + } if (updated || tx.block) self.emit('tx', tx, peer); diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 339fc1ab..901012cd 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -929,6 +929,65 @@ TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { return true; }; +TX.prototype.checkInputs = function checkInputs(spendHeight, ret) { + var total = new bn(0); + var i, input, coin, fee, value; + + if (!ret) + ret = {}; + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + coin = input.coin; + + if (coin.coinbase) { + if (spendHeight - coin.height < constants.tx.coinbaseMaturity) { + ret.reason = 'bad-txns-premature-spend-of-coinbase'; + ret.score = 0; + return false; + } + } + + if (coin.value.cmpn(0) < 0 || coin.value.cmp(constants.maxMoney) > 0) { + ret.reason = 'bad-txns-inputvalues-outofrange'; + ret.score = 100; + return false; + } + + total.iadd(coin.value); + } + + if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) { + ret.reason = 'bad-txns-inputvalues-outofrange'; + ret.score = 100; + return false; + } + + value = this.getOutputValue(); + + if (value.cmp(total) > 0) { + ret.reason = 'bad-txns-in-belowout' + ret.score = 100; + return false; + } + + fee = total.sub(value); + + if (fee.cmpn(0) < 0) { + ret.reason = 'bad-txns-fee-negative'; + ret.score = 100; + return false; + } + + if (fee.cmp(constants.maxMoney) > 0) { + ret.reason = 'bad-txns-fee-outofrange'; + ret.score = 100; + return false; + } + + return true; +}; + TX.prototype.maxSize = function maxSize() { return this.getVirtualSize(); }; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 6ede75d0..54a15327 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -1990,3 +1990,28 @@ if (utils.isBrowser) { return this.toArrayLike(Buffer, order, size); }; } + +/** + * VerifyError + */ + +function VerifyError(object, code, reason, score) { + Error.call(this); + if (Error.captureStackTrace) + Error.captureStackTrace(this, VerifyError); + this.type = 'VerifyError'; + this.hash = object.hash(); + this.height = object.height; + this.code = code; + this.reason = score === -1 ? null : reason; + this.score = score; + this.message = reason + + ' (code=' + code + + ', score=' + score + + ', height=' + this.height + + ', hash=' + utils.revHex(utils.toHex(this.hash)) + ')'; +} + +utils.inherits(VerifyError, Error); + +utils.VerifyError = VerifyError; diff --git a/test/node-test.js b/test/node-test.js index 597dc62f..8d5afc38 100644 --- a/test/node-test.js +++ b/test/node-test.js @@ -95,31 +95,29 @@ describe('Wallet', function() { f1.hint = 'f1'; fake.hint = 'fake'; - var peer = { sendReject: function() {} }; - - node.mempool.addTX(fake, peer, function(err) { + node.mempool.addTX(fake, function(err) { assert.noError(err); - node.mempool.addTX(t4, peer, function(err) { + node.mempool.addTX(t4, function(err) { assert.noError(err); node.mempool.getBalance(function(err, balance) { assert.noError(err); assert.equal(balance.toString(10), '0'); - node.mempool.addTX(t1, peer, function(err) { + node.mempool.addTX(t1, function(err) { assert.noError(err); node.mempool.getBalance(function(err, balance) { assert.noError(err); assert.equal(balance.toString(10), '60000'); - node.mempool.addTX(t2, peer, function(err) { + node.mempool.addTX(t2, function(err) { assert.noError(err); node.mempool.getBalance(function(err, balance) { assert.noError(err); assert.equal(balance.toString(10), '50000'); - node.mempool.addTX(t3, peer, function(err) { + node.mempool.addTX(t3, function(err) { assert.noError(err); node.mempool.getBalance(function(err, balance) { assert.noError(err); assert.equal(balance.toString(10), '22000'); - node.mempool.addTX(f1, peer, function(err) { + node.mempool.addTX(f1, function(err) { assert.noError(err); node.mempool.getBalance(function(err, balance) { assert.noError(err);