From 6194c66d8fe39fa1cbc8cf673ffdebfbb3c12cb5 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 26 Mar 2016 03:17:54 -0700 Subject: [PATCH] mempool work. accurate reject messages. --- lib/bcoin/chain.js | 60 +++-- lib/bcoin/ldb.js | 88 +++++-- lib/bcoin/mempool.js | 431 +++++++++++++++++++++++++------- lib/bcoin/mtx.js | 2 +- lib/bcoin/peer.js | 4 +- lib/bcoin/pool.js | 11 +- lib/bcoin/protocol/constants.js | 7 +- lib/bcoin/protocol/framer.js | 6 +- lib/bcoin/tx.js | 29 ++- lib/bcoin/txdb.js | 120 ++++----- lib/bcoin/walletdb.js | 1 - 11 files changed, 537 insertions(+), 222 deletions(-) diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 2b2a66ec..ba60c3f2 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -377,7 +377,8 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { } if (!block.verify(ret)) { - self.emit('verify-error', block, ret.reason, ret.score, peer); + self.emit('verify-error', + block, 'invalid', ret.reason, ret.score, peer); return done(null, false); } @@ -401,13 +402,15 @@ 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, 'time-too-old', 0, peer); + self.emit('verify-error', + block, 'invalid', 'time-too-old', 0, peer); return done(null, false); } if (block.bits !== self.getTarget(prev, block)) { utils.debug('Block is using wrong target: %s', block.rhash); - self.emit('verify-error', block, 'bad-diffbits', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-diffbits', 100, peer); return done(null, false); } @@ -426,7 +429,8 @@ 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, 'bad-version', 0, peer); + self.emit('verify-error', + block, 'obsolete', 'bad-version', 0, peer); return done(null, false); } @@ -434,7 +438,8 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // 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, 'bad-version', 0, peer); + self.emit('verify-error', + block, 'obsolete', 'bad-version', 0, peer); return done(null, false); } @@ -442,7 +447,8 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { // 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, 'bad-version', 0, peer); + self.emit('verify-error', + block, 'obsolete', 'bad-version', 0, peer); return done(null, false); } @@ -451,7 +457,8 @@ 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, 'bad-version', 0, peer); + self.emit('verify-error', + block, 'obsolete', 'bad-version', 0, peer); return done(null, false); } } @@ -493,7 +500,8 @@ 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, 'bad-cb-height', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-cb-height', 100, peer); return done(null, false); } } @@ -501,13 +509,15 @@ Chain.prototype._verify = function _verify(block, prev, peer, callback) { if (block.version >= 5 && segwit) { if (block.commitmentHash !== block.getCommitmentHash()) { utils.debug('Block failed witnessroot test: %s', block.rhash); - self.emit('verify-error', block, 'bad-blk-wit-length', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-blk-wit-length', 100, peer); return done(null, false); } } else { if (block.hasWitness()) { utils.debug('Unexpected witness data found: %s', block.rhash); - self.emit('verify-error', block, 'unexpected-witness', 100, peer); + self.emit('verify-error', + block, 'invalid', 'unexpected-witness', 100, peer); return done(null, false); } } @@ -523,7 +533,8 @@ 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, 'bad-txns-nonfinal', 10, peer); + self.emit('verify-error', + block, 'invalid', 'bad-txns-nonfinal', 10, peer); return done(null, false); } } @@ -557,7 +568,8 @@ 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))) { - self.emit('verify-error', block, 'bad-txns-BIP30', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-txns-BIP30', 100, peer); return next(null, false); } } @@ -606,7 +618,8 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, c if (sigops > constants.script.maxBlockSigops) { utils.debug('Block has too many sigops: %s', block.rhash); - self.emit('verify-error', block, 'bad-blk-sigops', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-blk-sigops', 100, peer); return callback(null, false); } @@ -629,7 +642,8 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, peer, c 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, 'bad-txns-inputs-missingorspent', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-txns-inputs-missingorspent', 100, peer); return callback(null, false); } @@ -682,7 +696,8 @@ Chain.prototype._checkReward = function _checkReward(block) { actual.iadd(block.txs[i].getFee()); if (claimed.cmp(actual) > 0) { - self.emit('verify-error', block, 'bad-cb-amount', 100, peer); + self.emit('verify-error', + block, 'invalid', 'bad-cb-amount', 100, peer); return false; } @@ -974,7 +989,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { chain: !!self.invalid[prevHash] }, peer); self.invalid[hash] = true; - self.emit('verify-error', block, 'duplicate', 0, peer); + self.emit('verify-error', + block, 'duplicate', 'duplicate', 0, peer); return done(); } @@ -990,7 +1006,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { seen: false, chain: false }, peer); - self.emit('verify-error', block, ret.reason, ret.score, peer); + self.emit('verify-error', + block, 'invalid', ret.reason, ret.score, peer); return done(); } @@ -1024,7 +1041,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { seen: true }, peer); - self.emit('verify-error', block, 'bad-prevblk', 0, peer); + self.emit('verify-error', + block, 'invalid', 'bad-prevblk', 0, peer); return done(); } @@ -1048,7 +1066,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { hash: hash, seen: false }, peer); - self.emit('verify-error', block, 'bad-prevblk', 0, peer); + self.emit('verify-error', + block, 'invalid', 'bad-prevblk', 0, peer); return done(); } @@ -1078,7 +1097,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { checkpoint: true }, peer); - self.emit('verify-error', block, 'checkpoint mismatch', 100, peer); + self.emit('verify-error', + block, 'checkpoint', 'checkpoint mismatch', 100, peer); return done(); } diff --git a/lib/bcoin/ldb.js b/lib/bcoin/ldb.js index 1ed24877..3eddea3f 100644 --- a/lib/bcoin/ldb.js +++ b/lib/bcoin/ldb.js @@ -10,34 +10,17 @@ var utils = bcoin.utils; var network = bcoin.protocol.network; var db = {}; -module.exports = function ldb(name, options) { - var file = bcoin.prefix + '/' + name + '-' + network.type + '.db'; - var backend = typeof options.db === 'string' - ? options.db - : process.env.BCOIN_DB; +/** + * LDB + */ + +function ldb(name, options) { + var file = getLocation(name); if (!db[file]) { if (!options) options = {}; - if (!backend || backend === 'leveldb') - backend = 'leveldown'; - else if (backend === 'rocksdb') - backend = 'rocksdown'; - else if (backend === 'lmdb') - backend = 'lmdb'; - else if (backend === 'memory') - backend = 'memdown'; - - if (bcoin.isBrowser && backend !== 'memdown') { - backend = require('level-js'); - } else { - if (backend !== 'memdown') - bcoin.ensurePrefix(); - - backend = require(backend); - } - db[file] = new LowlevelUp(file, { keyEncoding: 'ascii', valueEncoding: 'binary', @@ -59,12 +42,57 @@ module.exports = function ldb(name, options) { // optimizeCompaction: 'level', // memtableBudget: 512 << 20, - db: backend + db: getBackend(options.db) }); } return db[file]; -}; +} + +function getLocation(name) { + return bcoin.prefix + '/' + name + '-' + network.type + '.db'; +} + +function getBackend(backend) { + if (typeof backend !== 'string') + backend = process.env.BCOIN_DB; + + if (!backend || backend === 'leveldb') + backend = 'leveldown'; + else if (backend === 'rocksdb') + backend = 'rocksdown'; + else if (backend === 'lmdb') + backend = 'lmdb'; + else if (backend === 'memory') + backend = 'memdown'; + + // Require directly for browserify + if (backend === 'memdown') + return require('memdown'); + + if (bcoin.isBrowser) + return require('level-js'); + + bcoin.ensurePrefix(); + + return require(backend); +} + +function destroy(name, backend, callback) { + var file = getLocation(name); + + if (!callback) { + callback = backend; + backend = null; + } + + backend = getBackend(backend); + + if (!backend.destroy) + return utils.nextTick(callback); + + backend.destroy(file, callback); +} /** * LowlevelUp @@ -168,3 +196,13 @@ LowlevelUp.prototype.getProperty = function getProperty(name) { LowlevelUp.prototype.approximateSize = function approximateSize(start, end, callback) { return this.binding.approximateSize(start, end, callback); }; + +/** + * Expose + */ + +exports = ldb; +exports.LowlevelUp = LowlevelUp; +exports.destroy = destroy; + +module.exports = exports; diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index ddfe0d1a..757a51c0 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -9,6 +9,7 @@ var EventEmitter = require('events').EventEmitter; var bcoin = require('../bcoin'); var bn = require('bn.js'); var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; var utils = require('./utils'); var assert = utils.assert; var BufferWriter = require('./writer'); @@ -32,34 +33,20 @@ function Mempool(node, options) { this.node = node; this.chain = node.chain; - this.db = bcoin.ldb('mempool', { - db: 'memdown' - }); - - this.tx = new bcoin.txdb('m', this.db, { - indexSpent: true, - indexExtra: false, - indexAddress: false, - mapAddress: false, - verify: false - }); - this.loaded = false; - this.jobs = []; - this.busy = false; + this.locker = new bcoin.locker(this, this.add, 20 << 20); - this.pending = []; - this.pendingTX = {}; - this.pendingSize = 0; - this.pendingLimit = 20 << 20; - this.locker = new bcoin.locker(this, this.add, this.pendingLimit); + this.db = null; + this.tx = null; + this.size = 0; this.freeCount = 0; this.lastTime = 0; this.limitFree = this.options.limitFree !== false; this.limitFreeRelay = this.options.limitFreeRelay || 15; + this.relayPriority = this.options.relayPriority !== false; this.requireStandard = this.options.requireStandard !== false; this.rejectInsaneFees = this.options.rejectInsaneFees !== false; @@ -73,6 +60,10 @@ utils.inherits(Mempool, EventEmitter); Mempool.flags = constants.flags.STANDARD_VERIFY_FLAGS; Mempool.mandatory = constants.flags.MANDATORY_VERIFY_FLAGS; +Mempool.ANCESTOR_LIMIT = 25; +Mempool.MAX_MEMPOOL_SIZE = 300 << 20; +Mempool.MEMPOOL_EXPIRY = 72 * 60 * 60; + Mempool.prototype._lock = function _lock(func, args, force) { return this.locker.lock(func, args, force); }; @@ -83,20 +74,53 @@ Mempool.prototype.purgePending = function purgePending() { Mempool.prototype._init = function _init() { var self = this; + var unlock = this._lock(utils.nop, []); - if (this.db.loaded) { - this.loaded = true; - return; - } + bcoin.ldb.destroy('mempool', 'memdown', function(err) { + if (err) { + unlock(); + return self.emit('error', err); + } - this.db.once('open', function() { - self.loaded = true; - self.emit('open'); + self.db = bcoin.ldb('mempool', { + db: 'memdown' + }); + + self.tx = new bcoin.txdb('m', self.db, { + indexExtra: false, + indexAddress: false, + mapAddress: false, + verify: false + }); + + self.db.open(function(err) { + if (err) { + unlock(); + return self.emit('error', err); + } + self.dynamicMemoryUsage(function(err, size) { + if (err) + self.emit('error', err); + else + self.size = size; + + unlock(); + self.loaded = true; + self.emit('open'); + }); + }); }); }; +Mempool.prototype.dynamicMemoryUsage = function dynamicMemoryUsage(callback) { + return this.db.approximateSize('m', 'm~', callback); +}; + Mempool.prototype.open = function open(callback) { - return this.db.open(callback); + if (this.loaded) + return utils.nextTick(callback); + + return this.once('open', callback); }; Mempool.prototype.addBlock = function addBlock(block, callback, force) { @@ -107,14 +131,9 @@ Mempool.prototype.addBlock = function addBlock(block, callback, force) { callback = utils.wrap(callback, unlock); - this.open(function(err) { - if (err) - return callback(err); - - utils.forEachSerial(block.txs, function(tx, next) { - self.tx.removeUnchecked(tx, next); - }, callback); - }); + utils.forEachSerial(block.txs, function(tx, next) { + self.removeUnchecked(tx, next); + }, callback); }; Mempool.prototype.removeBlock = function removeBlock(block, callback, force) { @@ -126,10 +145,91 @@ Mempool.prototype.removeBlock = function removeBlock(block, callback, force) { callback = utils.wrap(callback, unlock); utils.forEachSerial(block.txs.slice().reverse(), function(tx, next) { - self.tx.addUnchecked(tx, next); + self.addUnchecked(tx, next); }, callback); }; +Mempool.prototype.limitMempoolSize = function limitMempoolSize(callback) { + var self = this; + + if (this.size <= Mempool.MAX_MEMPOOL_SIZE) + return callback(null, true); + + this.db.getRange({ + start: 0, + end: utils.now() - Mempool.MEMPOOL_EXPIRY + }, function(err, txs) { + if (err) + return callback(err); + + utils.forEachSerial(function(tx, next) { + self.removeUnchecked(tx, next); + }, function(err) { + if (err) + return callback(err); + + self.purgeOrphans(function(err) { + if (err) + return callback(err); + + return callback(self.size <= Mempool.MAX_MEMPOOL_SIZE); + }); + }); + }); +}; + +Mempool.prototype.purgeOrphans = function purgeOrphans(callback) { + var self = this; + var batch = this.db.batch(); + + callback = utils.ensure(callback); + + utils.forEachSerial(['m/D', 'm/d'], function(type, callback) { + var iter = self.db.iterator({ + gte: type, + lte: type + '~', + keys: true, + values: false, + fillCache: false, + keyAsBuffer: false + }); + + (function next() { + iter.next(function(err, key, value) { + if (err) { + return iter.end(function() { + callback(err); + }); + } + + if (key === undefined) + return iter.end(callback); + + batch.del(key); + + next(); + }); + })(); + }, function(err) { + if (err) + return callback(err); + + batch.write(function(err) { + if (err) + return callback(err); + + self.dynamicMemoryUsage(function(err, size) { + if (err) + return callback(err); + + self.size = size; + + return callback(); + }); + }); + }); +}; + Mempool.prototype.get = Mempool.prototype.getTX = function getTX(hash, callback) { if (hash instanceof bcoin.tx) @@ -197,18 +297,18 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { callback = utils.asyncify(callback); if (!this.checkTX(tx, peer)) - return callback(new VerifyError('CheckTransaction failed', -1)); + return callback(new VerifyError('invalid', 'CheckTransaction failed', -1)); if (tx.isCoinbase()) { - peer.sendReject(tx, 'coinbase', 100); - return callback(new VerifyError('coinbase as individual tx', 100)); + peer.sendReject(tx, 'invalid', 'coinbase', 100); + return callback(new VerifyError('invalid', 'coinbase', 100)); } ts = utils.now(); height = this.chain.height + 1; if (this.requireStandard && !tx.isStandard(Mempool.flags, ts, height, ret)) { - peer.sendReject(tx, ret.reason, 0); + peer.sendReject(tx, 'nonstandard', ret.reason, 0); return callback(new VerifyError(ret.reason, 0)); } @@ -216,33 +316,57 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { if (err) return callback(err); - if (exists) + if (exists) { + peer.sendReject(tx, 'alreadyknown', 'txn-already-in-mempool', 0); return callback(); + } self.tx.isDoubleSpend(tx, function(err, doubleSpend) { if (err) return callback(err); if (doubleSpend) { - peer.sendReject(tx, 'bad-txns-inputs-spent', 0); - return callback(new VerifyError('bad-txns-inputs-spent', 0)); + peer.sendReject(tx, 'duplicate', 'bad-txns-inputs-spent', 0); + return callback(new VerifyError( + 'duplicate', + 'bad-txns-inputs-spent', + 0)); } self.node.fillCoin(tx, function(err) { if (err) return callback(err); - if (!tx.hasPrevout()) + if (!tx.hasPrevout()) { + if (self.size > Mempool.MAX_MEMPOOL_SIZE) { + return callback( + new VerifyError('insufficientfee', + 'mempool full', + 0)); + } return self.storeOrphan(tx, callback); + } self.verify(tx, function(err) { if (err) { if (err.type === 'VerifyError' && err.score >= 0) - peer.sendReject(tx, err.reason, err.score); + peer.sendReject(tx, err.code, err.reason, err.score); return callback(err); } - self.addUnchecked(tx, peer, callback); + self.limitMempoolSize(function(err, result) { + if (err) + return callback(err); + + if (!result) { + return callback( + new VerifyError('insufficientfee', + 'mempool full', + 0)); + } + + self.addUnchecked(tx, peer, callback); + }); }); }); }); @@ -255,7 +379,9 @@ Mempool.prototype.addUnchecked = function addUnchecked(tx, peer, callback) { if (err) return callback(err); + self.size += tx.getSize(); self.emit('tx', tx); + self.emit('add tx', tx); utils.debug('Added tx %s to the mempool.', tx.rhash); @@ -274,15 +400,40 @@ Mempool.prototype.addUnchecked = function addUnchecked(tx, peer, callback) { }); }; +Mempool.prototype.removeUnchecked = function removeUnchecked(tx, callback) { + var self = this; + this.tx.removeUnchecked(tx, function(err) { + if (err) + return callback(err); + self.size -= tx.getSize(); + self.emit('remove tx', tx); + return callback(); + }); +}; + Mempool.prototype.verify = function verify(tx, callback) { var self = this; - var total, input, coin, i, fee, now; + var height = this.chain.height + 1; + var total, input, coin, i, fee, now, free, minFee; - if (this.requireStandard && !tx.isStandardInputs(Mempool.flags)) - return callback(new VerifyError('TX inputs are not standard.', -1)); + if (network.type !== 'segwit') { + if (tx.hasWitness()) + return callback(new VerifyError('nonstandard', 'no-witness-yet', 0)); + } - if (tx.getSigops(true) > constants.script.maxSigops) - return callback(new VerifyError('bad-txns-too-many-sigops', 0)); + if (this.requireStandard && !tx.isStandardInputs(Mempool.flags)) { + return callback(new VerifyError( + 'nonstandard', + 'bad-txns-nonstandard-inputs', + 0)); + } + + if (tx.getSigops(true) > constants.script.maxSigops) { + return callback(new VerifyError( + 'nonstandard', + 'bad-txns-too-many-sigops', + 0)); + } total = new bn(0); for (i = 0; i < tx.inputs.length; i++) { @@ -290,34 +441,61 @@ Mempool.prototype.verify = function verify(tx, callback) { coin = input.output; if (coin.coinbase) { - if (this.chain.height - coin.height < constants.tx.coinbaseMaturity) - return callback(new VerifyError('bad-txns-premature-spend-of-coinbase', 0)); + if (this.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('bad-txns-inputvalues-outofrange', 100)); + 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('bad-txns-inputvalues-outofrange', 100)); + 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('bad-txns-in-belowout', 100)); + return callback(new VerifyError('invalid', 'bad-txns-in-belowout', 100)); fee = total.sub(tx.getOutputValue()); if (fee.cmpn(0) < 0) - return callback(new VerifyError('bad-txns-fee-negative', 100)); + return callback(new VerifyError('invalid', 'bad-txns-fee-negative', 100)); if (fee.cmp(constants.maxMoney) > 0) - return callback(new VerifyError('bad-txns-fee-outofrange', 100)); + return callback(new VerifyError('invalid', 'bad-txns-fee-outofrange', 100)); - if (this.limitFree && fee.cmp(tx.getMinFee(true)) < 0) - return callback(new VerifyError('insufficient fee', 0)); + minFee = tx.getMinFee(); + if (fee.cmp(minFee) < 0) { + if (this.relayPriority && fee.cmpn(0) === 0) { + free = tx.isFree(height); + if (!free) { + return callback(new VerifyError( + 'insufficientfee', + 'insufficient priority', + 0)); + } + } else { + return callback(new VerifyError( + 'insufficientfee', + 'insufficient fee', + 0)); + } + } - if (this.limitFree && fee.cmpn(tx.getMinFee()) < 0) { + if (this.limitFree && free) { now = utils.now(); if (!this.lastTime) @@ -326,33 +504,85 @@ Mempool.prototype.verify = function verify(tx, callback) { this.freeCount *= Math.pow(1 - 1 / 600, now - this.lastTime); this.lastTime = now; - if (this.freeCount > this.limitFreeRelay * 10 * 1000) - return callback(new VerifyError('insufficient priority', 0)); + if (this.freeCount > this.limitFreeRelay * 10 * 1000) { + return callback(new VerifyError( + 'insufficientfee', + 'rate limited free transaction', + 0)); + } this.freeCount += tx.getVirtualSize(); } - if (this.rejectInsaneFees && fee.cmpn(tx.getMinFee().muln(10000)) > 0) - return callback(new VerifyError('TX has an insane fee.', -1)); + if (this.rejectInsaneFees && fee.cmp(minFee.muln(10000)) > 0) + return callback(new VerifyError('highfee', 'absurdly-high-fee', 0)); - // Do this in the worker pool. - tx.verifyAsync(null, true, Mempool.flags, function(err, result) { + this.countAncestors(tx, function(err, count) { if (err) return callback(err); - if (!result) { - return tx.verifyAsync(null, true, Mempool.mandatory, function(err, result) { - if (err) - return callback(err); - - if (!result) - return callback(new VerifyError('mandatory-script-verify-flag', 0)); - - return callback(new VerifyError('non-mandatory-script-verify-flag', 0)); - }); + if (count > Mempool.ANCESTOR_LIMIT) { + return callback(new VerifyError( + 'nonstandard', + 'too-long-mempool-chain', + 0)); } - return callback(); + // Do this in the worker pool. + tx.verifyAsync(null, true, Mempool.flags, function(err, result) { + if (err) + return callback(err); + + if (!result) { + return tx.verifyAsync(null, true, Mempool.mandatory, function(err, result) { + if (err) + return callback(err); + + if (!result) { + return callback(new VerifyError( + 'nonstandard', + 'mandatory-script-verify-flag', + 0)); + } + + return callback( + new VerifyError('nonstandard', + 'non-mandatory-script-verify-flag', + 0)); + }); + } + + return callback(); + }); + }); +}; + +Mempool.prototype.countAncestors = function countAncestors(tx, callback) { + var self = this; + var inputs = new Array(tx.inputs.length); + utils.forEachSerial(tx.inputs, function(input, next, i) { + inputs[i] = 0; + self.getTX(input.prevout.hash, function(err, tx) { + if (err) + return next(err); + + if (!tx) + return next(); + + self.countAncestors(tx, function(err, max) { + if (err) + return next(err); + + inputs[i] += max; + + next(); + }); + }); + }, function(err) { + if (err) + return callback(err); + + return callback(null, inputs.sort().pop()); }); }; @@ -507,7 +737,7 @@ Mempool.prototype.resolveOrphans = function resolveOrphans(tx, callback, force) }); }; -Mempool.prototype.getInv = function getInv(callback) { +Mempool.prototype.getSnapshot = function getSnapshot(callback) { return this.tx.getAllHashes(callback); }; @@ -516,60 +746,73 @@ Mempool.prototype.checkTX = function checkTX(tx, peer) { }; Mempool.checkTX = function checkTX(tx, peer) { - var i, input, output, size; - var total = new bn(0); var uniq = {}; + var total = new bn(0); + var i, input, output, size; if (!peer) peer = DUMMY_PEER; if (tx.inputs.length === 0) - return peer.sendReject(tx, 'bad-txns-vin-empty', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-vin-empty', 100); if (tx.outputs.length === 0) - return peer.sendReject(tx, 'bad-txns-vout-empty', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-vout-empty', 100); if (tx.getVirtualSize() > constants.block.maxSize) - return peer.sendReject(tx, 'bad-txns-oversize', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-oversize', 100); for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; + if (output.value.cmpn(0) < 0) - return peer.sendReject(tx, 'bad-txns-vout-negative', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-vout-negative', 100); + if (output.value.cmp(constants.maxMoney) > 0) - return peer.sendReject(tx, 'bad-txns-vout-toolarge', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-vout-toolarge', 100); + total.iadd(output.value); - if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) - return peer.sendReject(tx, 'bad-txns-txouttotal-toolarge', 100); + + if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) { + return peer.sendReject(tx, + 'invalid', + 'bad-txns-txouttotal-toolarge', + 100); + } } for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; if (uniq[input.prevout.hash]) - return peer.sendReject(tx, 'bad-txns-inputs-duplicate', 100); + return peer.sendReject(tx, 'invalid', 'bad-txns-inputs-duplicate', 100); uniq[input.prevout.hash] = true; } if (tx.isCoinbase()) { size = tx.inputs[0].script.getSize(); if (size < 2 || size > 100) - return peer.sendReject(tx, 'bad-cb-length', 100); + return peer.sendReject(tx, 'invalid', 'bad-cb-length', 100); } else { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; if (+input.prevout.hash === 0) - return peer.sendReject(tx, 'bad-txns-prevout-null', 10); + return peer.sendReject(tx, 'invalid', 'bad-txns-prevout-null', 10); } } return true; }; -function VerifyError(reason, score) { +/** + * 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; diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index 25c5f1cb..c1e6f5bb 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -882,7 +882,7 @@ MTX.prototype.selectCoins = function selectCoins(unspent, options) { // Calculate max possible size after signing. size = tx.maxSize(options.m, options.n); - // if (newkb == null && tx.isFree(size)) { + // if (newkb == null && tx.isFree(this.chain.height + 1, size)) { // fee = new bn(0); // break; // } diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index d6872b2c..724131fd 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -679,8 +679,8 @@ Peer.prototype.setMisbehavior = function setMisbehavior(dos) { return this.pool.setMisbehavior(this, dos); }; -Peer.prototype.sendReject = function sendReject(obj, reason, dos) { - return this.pool.reject(this, obj, reason, dos); +Peer.prototype.sendReject = function sendReject(obj, code, reason, dos) { + return this.pool.reject(this, obj, code, reason, dos); }; /** diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 0af8c29d..4e34a1e3 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -191,8 +191,8 @@ Pool.prototype._init = function _init() { } }); - this.chain.on('verify-error', function(block, reason, score, peer) { - peer.sendReject(block, reason, score); + 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) { @@ -1885,14 +1885,15 @@ Pool.prototype.isMisbehaving = function isMisbehaving(host) { return false; }; -Pool.prototype.reject = function reject(peer, obj, reason, dos) { +Pool.prototype.reject = function reject(peer, obj, code, reason, dos) { if (dos != null) peer.setMisbehavior(dos); - utils.debug('Rejecting %s %s: reason=%s', - obj.type, obj.hash('hex'), reason); + utils.debug('Rejecting %s %s: ccode=%s reason=%s', + obj.type, obj.hash('hex'), code, reason); peer.reject({ + ccode: code, reason: reason, data: obj.hash() }); diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 68d0574c..dd6e8ded 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -249,7 +249,12 @@ exports.reject = { nonstandard: 0x40, dust: 0x41, insufficientfee: 0x42, - checkpoint: 0x43 + checkpoint: 0x43, + // Internal codes (NOT FOR USE ON NETWORK) + internal: 0x100, + highfee: 0x100, + alreadyknown: 0x101, + conflict: 0x102 }; exports.rejectByVal = Object.keys(exports.reject).reduce(function(out, name) { diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index 26128e87..adade16b 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -580,9 +580,13 @@ Framer.headers = function _headers(block, writer) { Framer.reject = function reject(details, writer) { var p = new BufferWriter(writer); + var ccode = details.ccode; + + if (ccode >= constants.reject.internal) + ccode = constants.reject.invalid; p.writeVarString(details.message || '', 'ascii'); - p.writeU8(constants.reject[details.ccode] || constants.reject.malformed); + p.writeU8(constants.reject[ccode] || constants.reject.invalid); p.writeVarString(details.reason || '', 'ascii'); if (details.data) p.writeHash(details.data); diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 9dd7be52..14b3f886 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -916,10 +916,11 @@ TX.prototype.maxSize = function maxSize() { return this.getVirtualSize(); }; -TX.prototype.getPriority = function getPriority(size) { - var sum, i, input, age, height; +TX.prototype.getPriority = function getPriority(height, size) { + var sum, i, input, age; - height = this.height; + if (height == null) + height = this.height; if (height === -1) height = null; @@ -927,7 +928,9 @@ TX.prototype.getPriority = function getPriority(size) { if (!this.hasPrevout()) return new bn(0); - size = size || this.maxSize(); + if (size == null) + size = this.maxSize(); + sum = new bn(0); for (i = 0; i < this.inputs.length; i++) { @@ -950,29 +953,31 @@ TX.prototype.getPriority = function getPriority(size) { return sum.divn(size); }; -TX.prototype.isFree = function isFree(size) { +TX.prototype.isFree = function isFree(height, size) { var priority; if (!this.hasPrevout()) return false; - size = size || this.maxSize(); + if (height == null) + height = this.height; + + if (size == null) + size = this.maxSize(); if (size >= constants.tx.maxFreeSize) return false; - priority = this.getPriority(); + priority = this.getPriority(height, size); return priority.cmp(constants.tx.freeThreshold) > 0; }; -TX.prototype.getMinFee = function getMinFee(allowFree, size) { +TX.prototype.getMinFee = function getMinFee(size) { var fee; - size = size || this.maxSize(); - - if (allowFree && this.isFree(size)) - return new bn(0); + if (size == null) + size = this.maxSize(); fee = new bn(constants.tx.minFee).muln(size).divn(1000); diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 3d6998b6..299a9d48 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -1546,6 +1546,7 @@ TXPool.prototype.addUnchecked = function addUnchecked(tx, callback, force) { batch = this.db.batch(); batch.put(prefix + 't/t/' + hash, tx.toExtended()); + batch.put(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash, DUMMY); tx.getAddresses().forEach(function(address) { batch.put(prefix + 't/a/' + address + '/' + hash, DUMMY); @@ -1586,68 +1587,77 @@ TXPool.prototype.addUnchecked = function addUnchecked(tx, callback, force) { }); }; -TXPool.prototype.removeUnchecked = function removeUnchecked(tx, callback, force) { +TXPool.prototype.removeUnchecked = function removeUnchecked(hash, callback, force) { var self = this; var prefix = this.prefix + '/'; - var hash = tx.hash('hex'); var batch; - var unlock = this._lock(removeUnchecked, [tx, callback], force); + var unlock = this._lock(removeUnchecked, [hash, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); - batch = this.db.batch(); + if (hash.hash) + hash = hash.hash('hex'); - batch.del(prefix + 't/t/' + hash); - batch.del(prefix + 'D/' + hash); - - tx.getAddresses().forEach(function(address) { - batch.del(prefix + 't/a/' + address + '/' + hash); - }); - - tx.inputs.forEach(function(input) { - var key = input.prevout.hash + '/' + input.prevout.index; - var address; - - if (tx.isCoinbase()) - return; - - if (!input.output) - return; - - address = input.getAddress(); - - batch.del(prefix + 'u/t/' + key); - batch.del(prefix + 's/t/' + key); - - if (address) - batch.del(prefix + 'u/a/' + address + '/' + key); - }); - - tx.outputs.forEach(function(output, i) { - var key = hash + '/' + i; - var address = output.getAddress(); - - batch.del(prefix + 'u/t/' + key); - - if (address) - batch.del(prefix + 'u/a/' + address + '/' + key); - }); - - batch.write(function(err) { + this.getTX(hash, function(err, tx) { if (err) return callback(err); - self.emit('remove tx', tx); - return callback(); + + if (!tx) + return callback(); + + batch = self.db.batch(); + + batch.del(prefix + 't/t/' + hash); + batch.del(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash); + batch.del(prefix + 'D/' + hash); + + tx.getAddresses().forEach(function(address) { + batch.del(prefix + 't/a/' + address + '/' + hash); + }); + + tx.inputs.forEach(function(input) { + var key = input.prevout.hash + '/' + input.prevout.index; + var address; + + if (tx.isCoinbase()) + return; + + if (!input.output) + return; + + address = input.getAddress(); + + batch.del(prefix + 'u/t/' + key); + batch.del(prefix + 's/t/' + key); + + if (address) + batch.del(prefix + 'u/a/' + address + '/' + key); + }); + + tx.outputs.forEach(function(output, i) { + var key = hash + '/' + i; + var address = output.getAddress(); + + batch.del(prefix + 'u/t/' + key); + + if (address) + batch.del(prefix + 'u/a/' + address + '/' + key); + }); + + batch.write(function(err) { + if (err) + return callback(err); + self.emit('remove tx', tx); + return callback(); + }); }); }; -TXPool.prototype.zap = function zap(tip, callback, force) { +TXPool.prototype.zap = function zap(now, age, callback, force) { var self = this; - var now = tip.ts; - var age = 10 * 60 * 60; var unlock = this._lock(zap, [tip, callback], force); if (!unlock) @@ -1655,23 +1665,13 @@ TXPool.prototype.zap = function zap(tip, callback, force) { callback = utils.wrap(callback, unlock); - // return this.getRange(null, { - // start: 0, - // end: now - age - // }, function(err, txs) { - // }); - - this.getPending(function(err, txs) { + return this.getRange(null, { + start: 0, + end: now - age + }, function(err, txs) { if (err) return callback(err); - txs = txs.filter(function(tx) { - return now > tx.ps + age; - }); - - if (txs.length === 0) - return callback(); - self.fillTX(txs, function(err) { if (err) return callback(err); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 3bde6879..55904db7 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -98,7 +98,6 @@ WalletDB.prototype._init = function _init() { }); this.tx = new bcoin.txdb('w', this.db, { - indexSpent: true, indexExtra: true, indexAddress: true, mapAddress: true,