diff --git a/lib/bcoin.js b/lib/bcoin.js index 49cb4462..1f396176 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -24,6 +24,7 @@ bcoin.debugLogs = +process.env.BCOIN_DEBUG === 1; bcoin.debugFile = +process.env.BCOIN_DEBUGFILE !== 0; bcoin.profile = +process.env.BCOIN_PROFILE === 1; bcoin.fresh = +process.env.BCOIN_FRESH === 1; +bcoin.useWorkers = +process.env.BCOIN_WORKERS === 1; bcoin.ensurePrefix = function ensurePrefix() { if (bcoin.isBrowser) @@ -122,5 +123,8 @@ bcoin.miner = require('./bcoin/miner'); bcoin.http = !bcoin.isBrowser ? require('./bcoin/ht' + 'tp') : null; +bcoin.workers = bcoin.useWorkers && !bcoin.isBrowser + ? require('./bcoin/work' + 'ers') + : null; bcoin.protocol.network.set(process.env.BCOIN_NETWORK || 'main'); diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 05ba3202..04479c1c 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -674,6 +674,9 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac 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)', @@ -694,7 +697,16 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac } } - return callback(null, true); + // Disable. Use workers and verifyAsync for now. + // return callback(null, true); + + if (!scriptCheck) + return callback(null, true); + + // Verify all txs in parallel. + utils.every(block.txs, function(tx, next) { + tx.verifyAsync(null, true, flags, next); + }, callback); }); }; diff --git a/lib/bcoin/coin.js b/lib/bcoin/coin.js index 2601ff80..46a1c8cc 100644 --- a/lib/bcoin/coin.js +++ b/lib/bcoin/coin.js @@ -75,6 +75,10 @@ Coin.prototype.getSize = function getSize() { return 4 + 4 + 8 + this.script.getSize() + 32 + 4 + 1; }; +Coin.prototype.isCoinbase = function isCoinbase() { + return false; +}; + Coin.prototype.getConfirmations = function getConfirmations(height) { var top; diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 6d9fb09b..9e5b17f6 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -207,108 +207,150 @@ Fullnode.prototype.getFullBlock = function getFullBlock(hash, callback) { }; Fullnode.prototype.getCoin = function getCoin(hash, index, callback) { - var coin; - - callback = utils.asyncify(callback); - - coin = this.mempool.getCoin(hash, index); - if (coin) - return callback(null, coin); - - if (this.mempool.isSpent(hash, index)) - return callback(null, null); - - this.chain.db.getCoin(hash, index, function(err, coin) { + var self = this; + this.mempool.getCoin(hash, index, function(err, coin) { if (err) return callback(err); - if (!coin) - return callback(); + if (coin) + return callback(null, coin); - return callback(null, coin); + self.chain.db.getCoin(hash, index, function(err, coin) { + if (err) + return callback(err); + + if (!coin) + return callback(); + + self.mempool.isSpent(hash, index, function(err, spent) { + if (err) + return callback(err); + + if (spent) + return callback(); + + return callback(null, coin); + }); + }); }); }; Fullnode.prototype.getCoinByAddress = function getCoinByAddress(addresses, callback) { var self = this; - var mempool; - - callback = utils.asyncify(callback); - - mempool = this.mempool.getCoinsByAddress(addresses); - - this.chain.db.getCoinsByAddress(addresses, function(err, coins) { + this.mempool.getCoinsByAddress(addresses, function(err, coins) { if (err) return callback(err); - return callback(null, mempool.concat(coins.filter(function(coin) { - if (self.mempool.isSpent(coin.hash, coin.index)) - return false; - return true; - }))); + self.chain.db.getCoinsByAddress(addresses, function(err, blockCoins) { + if (err) + return callback(err); + + utils.forEach(blockCoins, function(coin, next) { + self.mempool.isSpent(coin.hash, coin.index, function(err, spent) { + if (err) + return callback(err); + + if (!spent) + coins.push(coin); + + return next(); + }); + }, function(err) { + if (err) + return callback(err); + return callback(null, coins); + }); + }); }); }; Fullnode.prototype.getTX = function getTX(hash, callback) { - var tx; + var self = this; - callback = utils.asyncify(callback); - - tx = this.mempool.getTX(hash); - if (tx) - return callback(null, tx); - - this.chain.db.getTX(hash, function(err, tx) { + this.mempool.getTX(hash, function(err, tx) { if (err) return callback(err); - if (!tx) - return callback(); + if (tx) + return callback(null, tx); - return callback(null, tx); + self.chain.db.getTX(hash, function(err, tx) { + if (err) + return callback(err); + + if (!tx) + return callback(); + + return callback(null, tx); + }); + }); +}; + +Fullnode.prototype.hasTX = function hasTX(hash, callback) { + var self = this; + return this.getTX(hash, function(err, tx) { + if (err) + return callback(err); + return callback(null, !!tx); }); }; Fullnode.prototype.isSpent = function isSpent(hash, index, callback) { - callback = utils.asyncify(callback); + var self = this; - if (this.mempool.isSpent(hash, index)) - return callback(null, true); - - this.chain.db.isSpent(hash, index, callback); -}; - -Fullnode.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { - var mempool; - - callback = utils.asyncify(callback); - - mempool = this.mempool.getTXByAddress(addresses); - - this.chain.db.getTXByAddress(addresses, function(err, txs) { + this.mempool.isSpent(hash, index, function(err, spent) { if (err) return callback(err); - return callback(null, mempool.concat(txs)); + if (spent) + return callback(null, true); + + self.chain.db.isSpent(hash, index, callback); + }); +}; + +Fullnode.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { + var self = this; + + this.mempool.getTXByAddress(addresses, function(err, mempool) { + if (err) + return callback(err); + + self.chain.db.getTXByAddress(addresses, function(err, txs) { + if (err) + return callback(err); + + return callback(null, mempool.concat(txs)); + }); }); }; Fullnode.prototype.fillCoin = function fillCoin(tx, callback) { - callback = utils.asyncify(callback); + var self = this; - if (this.mempool.fillCoin(tx)) - return callback(); + this.mempool.fillCoin(tx, function(err, filled) { + if (err) + return callback(err); - this.chain.db.fillCoin(tx, callback); + if (filled) + return callback(null, tx); + + self.chain.db.fillCoin(tx, callback); + }); }; Fullnode.prototype.fillTX = function fillTX(tx, callback) { - callback = utils.asyncify(callback); + var self = this; - if (this.mempool.fillTX(tx)) - return callback(); + this.mempool.fillTX(tx, function(err, filled) { + if (err) + return callback(err); - this.chain.db.fillTX(tx, callback); + if (filled) + return callback(null, tx); + + self.chain.db.fillTX(tx, callback); + }); }; /** diff --git a/lib/bcoin/ldb.js b/lib/bcoin/ldb.js index 591e798f..4dacb10a 100644 --- a/lib/bcoin/ldb.js +++ b/lib/bcoin/ldb.js @@ -15,7 +15,9 @@ module.exports = function ldb(name, options) { var backend = process.env.BCOIN_DB; if (!db[file]) { - if (bcoin.isBrowser) { + if (options.db) { + backend = options.db; + } else if (bcoin.isBrowser) { backend = require('level-js'); } else { if (!backend || backend === 'leveldb') diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index 991e12de..16f38f96 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -28,6 +28,13 @@ function Mempool(node, options) { this.options = options; this.node = node; this.chain = node.chain; + this.db = node.chain.db; + this.tx = new bcoin.txdb('m', this.db, { + indexSpent: true, + indexExtra: false, + indexAddress: false, + mapAddress: false + }); this.txs = {}; this.spent = {}; @@ -36,6 +43,18 @@ function Mempool(node, options) { this.count = 0; this.locked = false; this.loaded = false; + this.jobs = []; + this.busy = false; + this.pending = []; + this.pendingTX = {}; + this.pendingSize = 0; + this.pendingLimit = 20 << 20; + this.freeCount = 0; + this.lastTime = 0; + this.limitFreeRelay = this.options.limitFreeRelay || 15; + this.requireStandard = this.options.requireStandard !== false; + this.limitFree = this.options.limitFree !== false; + this.rejectInsaneFees = this.options.rejectInsaneFees !== false; Mempool.global = this; @@ -44,443 +63,349 @@ function Mempool(node, options) { utils.inherits(Mempool, EventEmitter); +Mempool.prototype._lock = function _lock(func, args, force) { + var self = this; + var block, called; + + if (force) { + assert(this.busy); + return function unlock() { + assert(!called); + called = true; + }; + } + + if (this.busy) { + if (func === Mempool.prototype.add) { + tx = args[0]; + this.pending.push(tx); + this.pendingTX[tx.hash('hex')] = true; + this.pendingSize += tx.getSize(); + if (this.pendingSize > this.pendingLimit) { + this.purgePending(); + return; + } + } + this.jobs.push([func, args]); + return; + } + + this.busy = true; + + return function unlock() { + var item, tx; + + assert(!called); + called = true; + + self.busy = false; + + if (func === Chain.prototype.add) { + if (self.pending.length === 0) + self.emit('flush'); + } + + if (self.jobs.length === 0) + return; + + item = self.jobs.shift(); + + if (item[0] === Mempool.prototype.add) { + tx = item[1][0]; + assert(tx === self.pending.shift()); + delete self.pendingTX[tx.hash('hex')]; + self.pendingSize -= tx.getSize(); + } + + item[0].apply(self, item[1]); + }; +}; + +Mempool.prototype.purgePending = function purgePending() { + var self = this; + + utils.debug('Warning: %dmb of pending txs. Purging.', + utils.mb(this.pendingSize)); + + this.pending.forEach(function(tx) { + delete self.pendingTX[tx.hash('hex')]; + }); + + this.pending.length = 0; + this.pendingSize = 0; + + this.jobs = this.jobs.filter(function(item) { + return item[0] !== Mempool.prototype.add; + }); +}; + Mempool.prototype._init = function _init() { var self = this; - utils.nextTick(function() { + + if (this.db.loaded) { + this.loaded = true; + return; + } + + this.db.once('open', function() { self.loaded = true; self.emit('open'); }); }; Mempool.prototype.open = function open(callback) { - if (this.loaded) - return utils.nextTick(callback); - - this.once('open', callback); + return this.db.open(callback); }; Mempool.prototype.addBlock = function addBlock(block) { var self = this; + callback = utils.ensure(callback); // Remove now-mined transactions - block.txs.forEach(function(tx) { - var mtx = self.get(tx); - if (!mtx) - return; - - mtx.ps = 0; - mtx.ts = block.ts; - mtx.block = block.hash('hex'); - mtx.network = true; - - self.removeTX(mtx); - }); + // XXX should batch this + utils.forEachSerial(block.txs.slice().reverse(), function(tx, next) { + self.tx.remove(tx, next); + }, callback); }; -Mempool.prototype.removeBlock = function removeBlock(block) { +Mempool.prototype.removeBlock = function removeBlock(block, callback) { var self = this; - block.txs.forEach(function(tx) { - var hash = tx.hash('hex'); - // Remove anything that tries to redeem these outputs - tx.outputs.forEach(function(output, i) { - var mtx = self.spent[hash + '/' + i]; - if (!mtx) - return; - - self.removeTX(mtx); - }); - // Add transaction back into mempool - // tx = tx.clone(); - tx.ps = utils.now(); - tx.ts = 0; - tx.block = null; - tx.network = true; - self.addTX(tx); - }); + callback = utils.ensure(callback); + // XXX should batch this + utils.forEachSerial(block.txs, function(tx, next) { + self.tx.add(tx, next); + }, callback); }; Mempool.prototype.get = -Mempool.prototype.getTX = function getTX(hash) { +Mempool.prototype.getTX = function getTX(hash, callback) { if (hash instanceof bcoin.tx) hash = hash.hash('hex'); - return this.txs[hash]; + return this.tx.getTX(hash, index, callback); }; -Mempool.prototype.getCoin = function getCoin(hash, index) { - var tx = this.get(hash); - if (!tx) - return; - - return bcoin.coin(tx, index); +Mempool.prototype.getCoin = function getCoin(hash, index, callback) { + return this.tx.getCoin(hash, index, callback); }; -Mempool.prototype.isSpent = function isSpent(hash, index) { - return !!this.spent[hash + '/' + index]; +Mempool.prototype.isSpent = function isSpent(hash, index, callback) { + return this.tx.isSpent(hash, index, callback); }; -Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses) { - var txs = this.getByAddress(addresses); - return txs.reduce(function(out, tx) { - return out.concat(tx.outputs.map(function(output, i) { - return bcoin.coin(tx, i); - })); - }, []); +Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses, callback) { + return this.tx.getCoinsByAddress(addresses, callback); }; Mempool.prototype.getByAddress = Mempool.prototype.getTXByAddress = function getTXByAddress(addresses) { - var self = this; - var txs = []; - var uniq = {}; - - if (typeof addresses === 'string') - addresses = [addresses]; - - addresses = utils.uniqs(addresses); - - addresses.forEach(function(address) { - var map = self.addresses[address]; - if (!map) - return; - - Object.keys(map).forEach(function(hash) { - var tx; - - if (uniq[hash]) - return; - - uniq[hash] = true; - - tx = self.get(hash); - assert(tx); - - txs.push(tx); - }); - }); - - return txs; + return this.tx.getTXByAddress(addresses, callback); }; -Mempool.prototype.fillCoin = -Mempool.prototype.fillTX = function fillTX(tx) { - var i, input, total; - - total = 0; - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - - if (input.output) { - total++; - continue; - } - - if (this.hasTX(input.prevout.hash)) { - input.output = this.getCoin(input.prevout.hash, input.prevout.index); - total++; - } - } - - return total === tx.inputs.length; +Mempool.prototype.fillTX = function fillTX(tx, callback) { + return this.tx.fillTX(tx, callback); }; -Mempool.prototype.getAll = function getAll() { - return Object.keys(this.txs).map(function(key) { - return this.txs[key]; - }, this); +Mempool.prototype.fillCoin = function fillCoin(tx, callback) { + return this.tx.fillCoin(tx, callback); }; Mempool.prototype.has = -Mempool.prototype.hasTX = function hasTX(hash) { - return !!this.get(hash); +Mempool.prototype.hasTX = function hasTX(hash, callback) { + return this.get(hash, function(err, tx) { + if (err) + return callback(err); + return callback(null, !!tx); + }); }; Mempool.prototype.add = -Mempool.prototype.addTX = function addTX(tx, peer, callback) { +Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { var self = this; var flags = constants.flags.STANDARD_VERIFY_FLAGS; - var hash = tx.hash('hex'); + var hash, ts, height, now; + var ret = {}; + + var unlock = this._lock(addTX, [tx, peer, callback], force); + if (!unlock) + return; + + hash = tx.hash('hex'); assert(tx.ts === 0); + callback = utils.wrap(callback, unlock); callback = utils.asyncify(callback); - if (this.locked) - return callback(new Error('Mempool is locked.')); - - if (this.count >= 50000) - return callback(new Error('Mempool is full.')); - - if (this.size >= 20 * 1024 * 1024) - return callback(new Error('Mempool is full.')); - - if (this.txs[hash]) - return callback(new Error('Already have TX.')); - - if (tx.isCoinbase()) - return callback(new Error('What?')); - if (!this.checkTX(tx, peer)) - return callback(new Error('TX failed checkTX.')); + return callback(new Error('CheckTransaction failed')); - assert(tx.ts === 0); + if (tx.isCoinbase()) { + this.reject(peer, tx, 'coinbase', 100); + return callback(new Error('coinbase as individual tx')); + } - this._lockTX(tx); + ts = utils.now(); + height = this.chain.height + 1; - this.chain.fillCoin(tx, function(err) { - var i, input, output, dup, height, ts, priority; - - self._unlockTX(tx); + if (self.requireStandard && !tx.isStandard(flags, ts, height, ret)) { + self.reject(peer, tx, ret.reason, 0); + return callback(new Error('TX is not standard.')); + } + this.node.hasTX(tx, function(err, exists) { if (err) return callback(err); - // Do this in the future. - // tx = self.fillCoin(tx); + if (exists) + return callback(); - if (!tx.hasPrevout()) { - return callback(new Error('Previous outputs not found.')); - peer.reject({ - data: tx.hash(), - reason: 'no-prevout' - }); - return callback(new Error('Previous outputs not found.')); - } + self.node.fillCoin(tx, function(err) { + var i, input, output, total, fee, coin; - if (!tx.isStandard(flags)) { - return callback(new Error('TX is not standard.')); - peer.reject({ - data: tx.hash(), - reason: 'non-standard' - }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX is not standard.')); - } + if (err) + return callback(err); - if (!tx.isStandardInputs(flags)) { - return callback(new Error('TX inputs are not standard.')); - peer.reject({ - data: tx.hash(), - reason: 'non-standard-inputs' - }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX inputs are not standard.')); - } + if (!tx.hasPrevout()) { + // Store as orphan: + // return self.tx.add(tx, callback); + return callback(new Error('No prevouts yet.')); + } - if (tx.getOutputValue().cmp(tx.getInputValue()) > 0) { - return callback(new Error('TX is spending coins that it does not have.')); - peer.reject({ - data: tx.hash(), - reason: 'nonexistent-coins' - }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX is spending coins that it does not have.')); - } + if (self.requireStandard && !tx.isStandardInputs(flags)) + return callback(new Error('TX inputs are not standard.')); - height = self.node.pool.chain.height + 1; - ts = utils.now(); - if (!tx.isFinal(height, ts)) { - return callback(new Error('TX is not final.')); - peer.reject({ - data: tx.hash(), - reason: 'not-final' - }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX is not final.')); - } + if (tx.getSigops(true) > constants.script.maxSigops) { + self.reject(peer, tx, 'bad-txns-too-many-sigops', 0); + return callback(new Error('TX has too many sigops.')); + } - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - dup = self.spent[input.prevout.hash + '/' + input.prevout.index]; - if (dup) { - // Replace-by-fee - if (input.sequence === 0xffffffff - 1) { - if (dup.getFee().cmp(tx.getFee()) < 0) { - self.remove(dup); - continue; + total = new bn(0); + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + coin = input.coin; + + if (coin.isCoinbase()) { + if (self.chain.height - coin.height < constants.tx.coinbaseMaturity) { + self.reject(peer, tx, 'bad-txns-premature-spend-of-coinbase', 0); + return callback(new Error('Tried to spend coinbase prematurely.')); } } - return callback(new Error('TX is double spending.')); - peer.reject({ - data: tx.hash(), - reason: 'double-spend' - }); - return callback(new Error('TX is double spending.')); - } - } - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - if (output.value.cmpn(0) < 0) { - return callback(new Error('TX is spending negative coins.')); - peer.reject({ - data: tx.hash(), - reason: 'negative-value' - }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX is spending negative coins.')); - } - } + if (coin.value.cmpn(0) < 0 || coin.value.cmp(constants.maxMoney) > 0) + return self.reject(peer, tx, 'bad-txns-inputvalues-outofrange', 100); - if (!tx.verify(null, true, flags)) { - return callback(new Error('TX did not verify.')); - peer.reject({ - data: tx.hash(), - reason: 'script-failed' + total.iadd(coin.value); + } + + if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) + return self.reject(peer, tx, 'bad-txns-inputvalues-outofrange', 100); + + if (tx.getOutputValue().cmp(total) > 0) { + self.reject(peer, tx, 'bad-txns-in-belowout', 100); + return callback(new Error('TX is spending coins it does not have.')); + } + + fee = total.subn(tx.getOutputValue()); + + if (fee.cmpn(0) < 0) { + self.reject(peer, tx, 'bad-txns-fee-negative', 100); + return callback(new Error('TX has a negative fee.')); + } + + if (fee.cmp(constants.maxMoney) > 0) { + return self.reject(peer, tx, 'bad-txns-fee-outofrange', 100); + return callback(new Error('TX has a fee higher than max money.')); + } + + if (self.limitFree && fee.cmp(tx.getMinFee(true)) < 0) { + self.reject(peer, tx, 'insufficient fee', 0); + return callback(new Error('Insufficient fee.')); + } + + if (self.limitFree && fee.cmpn(tx.getMinFee()) < 0) { + now = utils.now(); + + if (!self.lastTime) + self.lastTime = now; + + self.freeCount *= Math.pow(1 - 1 / 600, now - self.lastTime); + self.lastTime = now; + + if (self.freeCount > self.limitFreeRelay * 10 * 1000) { + self.reject(peer, tx, 'insufficient priority', 0); + return callback(new Error('Too many free txs at once!')); + } + + self.freeCount += tx.getVirtualSize(); + } + + if (self.rejectInsaneFees && fee.cmpn(tx.getMinFee().muln(10000)) > 0) + return callback(new Error('TX has an insane fee.')); + + // Do this in the worker pool. + tx.verifyAsync(null, true, flags, function(err, result) { + if (err) + return callback(err); + + if (!result) { + // Just say it's non-mandatory for now. + self.reject(peer, tx, 'non-mandatory-script-verify-flag', 0); + return callback(new Error('TX did not verify.')); + } + + self.tx.add(tx, function(err) { + if (err) { + if (err.message === 'Transaction is double-spending.') { + self.reject(peer, tx, 'bad-txns-inputs-spent', 0); + } + return callback(err); + } + + self.emit('tx', tx); + + return callback(); + }); }); - self.node.pool.setMisbehavior(peer, 100); - return callback(new Error('TX did not verify.')); - } - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - self.spent[input.prevout.hash + '/' + input.prevout.index] = tx; - self.size += input.output.getSize(); - } - - // Possibly do something bitcoinxt-like here with priority - priority = tx.getPriority(); - - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); - - if (type === 'pubkey' || type === 'multisig') - address = null; - - if (!address) - return; - - if (!self.addresses[address]) - self.addresses[address] = {}; - - self.addresses[address][hash] = true; }); - - tx.outputs.forEach(function(output) { - var type = output.getType(); - var address = output.getAddress(); - - if (type === 'pubkey' || type === 'multisig') - address = null; - - if (!address) - return; - - if (!self.addresses[address]) - self.addresses[address] = {}; - - self.addresses[address][hash] = true; - }); - - self.txs[hash] = tx; - self.count++; - self.size += tx.getSize(); - - self.emit('tx', tx); }); }; -// Lock a tx to prevent race conditions -Mempool.prototype._lockTX = function _lockTX(tx) { - var hash = tx.hash('hex'); - var i, input, id; - - if (!this.txs[hash]) - this.txs[hash] = tx; - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - id = input.prevout.hash + '/' + input.prevout.index; - if (!this.spent[id]) - this.spent[id] = tx; - } -}; - -Mempool.prototype._unlockTX = function _unlockTX(tx) { - var hash = tx.hash('hex'); - var i, input, id; - - if (this.txs[hash] === tx) - delete this.txs[hash]; - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - id = input.prevout.hash + '/' + input.prevout.index; - if (this.spent[id] === tx) - delete this.spent[id]; - } +Mempool.prototype.getInv = function getInv(callback) { + return this.tx.getAllHashes(callback); }; Mempool.prototype.remove = -Mempool.prototype.removeTX = function removeTX(hash, callback) { +Mempool.prototype.removeTX = function removeTX(hash, callback, force) { var self = this; - var tx, input, id, i; - callback = utils.asyncify(callback); + var unlock = this._lock(removeTX, [hash, callback], force); + if (!unlock) + return; - if (hash instanceof bcoin.tx) - hash = hash.hash('hex'); - - tx = this.txs[hash]; - - if (!tx) - return callback(new Error('TX does not exist in mempool.')); - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - id = input.prevout.hash + '/' + input.prevout.index; - if (this.spent[id] === tx) - delete this.spent[id]; + function getTX() { + if (hash.hash) { + hash = hash.hash('hex'); + return self.getTX(hash, function(err, tx) { + if (err) + return callback(err); + if (!tx) + return callback(); + return self.node.fillTX(hash, callback); + }); + } + return callback(null, hash); } - tx.inputs.forEach(function(input) { - var type = input.getType(); - var address = input.getAddress(); + getTX(function(err, tx) { + if (err) + return callback(err); - if (type === 'pubkey' || type === 'multisig') - address = null; + self.tx.remove(tx, function(err) { + if (err) + return callback(err); - if (!address) - return; - - if (self.addresses[address]) { - delete self.addresses[address][hash]; - if (Object.keys(self.addresses[address]).length === 0) - delete self.addresses[address]; - } + self.emit('remove tx', tx); + }); }); - - tx.outputs.forEach(function(output) { - var type = output.getType(); - var address = output.getAddress(); - - if (type === 'pubkey' || type === 'multisig') - address = null; - - if (!address) - return; - - if (self.addresses[address]) { - delete self.addresses[address][hash]; - if (Object.keys(self.addresses[address]).length === 0) - delete self.addresses[address]; - } - }); - - delete this.txs[hash]; - this.count--; - this.size -= tx.getSize(); - this.emit('remove tx', tx); -}; - -// Need to lock the mempool when -// downloading a new block. -Mempool.prototype.lock = function lock() { - this.locked = true; -}; - -Mempool.prototype.unlock = function unlock() { - this.locked = false; }; Mempool.prototype.checkTX = function checkTX(tx, peer) { @@ -489,57 +414,60 @@ Mempool.prototype.checkTX = function checkTX(tx, peer) { var uniq = {}; if (tx.inputs.length === 0) - return this.reject(peer, tx, 'bad-txns-vin-empty'); + return this.reject(peer, tx, 'bad-txns-vin-empty', 100); if (tx.outputs.length === 0) - return this.reject(peer, tx, 'bad-txns-vout-empty'); + return this.reject(peer, tx, 'bad-txns-vout-empty', 100); if (tx.getSize() > constants.block.maxSize) - return this.reject(peer, tx, 'bad-txns-oversize'); + return this.reject(peer, tx, 'bad-txns-oversize', 100); for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; if (output.value.cmpn(0) < 0) - return this.reject(peer, tx, 'bad-txns-vout-negative'); + return this.reject(peer, tx, 'bad-txns-vout-negative', 100); if (output.value.cmp(constants.maxMoney) > 0) - return this.reject(peer, tx, 'bad-txns-vout-toolarge'); + return this.reject(peer, tx, 'bad-txns-vout-toolarge', 100); total.iadd(output.value); if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney)) - return this.reject(peer, tx, 'bad-txns-txouttotal-toolarge'); + return this.reject(peer, tx, 'bad-txns-txouttotal-toolarge', 100); } for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; if (uniq[input.out.hash]) - return this.reject(peer, tx, 'bad-txns-inputs-duplicate'); + return this.reject(peer, tx, 'bad-txns-inputs-duplicate', 100); uniq[input.out.hash] = true; } if (tx.isCoinbase()) { size = bcoin.script.getSize(tx.inputs[0].script); if (size < 2 || size > 100) - return this.reject(peer, tx, 'bad-cb-length'); + return this.reject(peer, tx, 'bad-cb-length', 100); } else { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; if (+input.out.hash === 0) - return this.reject(peer, tx, 'bad-txns-prevout-null'); + return this.reject(peer, tx, 'bad-txns-prevout-null', 10); } } return true; }; -Mempool.prototype.reject = function reject(peer, obj, reason) { - return false; +Mempool.prototype.reject = function reject(peer, obj, reason, dos) { + utils.debug('Rejecting TX %s. Reason=%s.', obj.hash('hex'), reason); + + if (dos != null) + this.node.pool.setMisbehavior(peer, dos); if (!peer) return false; - peer.reject({ - reason: reason, - data: obj.hash ? obj.hash() : [] - }); + // peer.reject({ + // reason: reason, + // data: obj.hash ? obj.hash() : [] + // }); return false; }; diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 00a7f28f..68d0574c 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -218,7 +218,8 @@ exports.tx = { minFee: 10000, bareMultisig: true, freeThreshold: exports.coin.muln(144).divn(250), - maxFreeSize: 1000 + maxFreeSize: 1000, + coinbaseMaturity: 100 }; exports.tx.dustThreshold = new bn(182) diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index eb561050..74e69d3f 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -379,6 +379,19 @@ TX.prototype.verify = function verify(index, force, flags) { }, this); }; +TX.prototype.verifyAsync = function verifyAsync(index, force, flags, callback) { + var self = this; + utils.nextTick(function() { + var res; + try { + res = self.verify(index, force, flags); + } catch (e) { + return callback(e); + } + return callback(null, res); + }); +}; + TX.prototype.isCoinbase = function isCoinbase() { return this.inputs.length === 1 && +this.inputs[0].prevout.hash === 0; }; @@ -677,32 +690,51 @@ TX.prototype.getSigops = function getSigops(scriptHash, accurate) { return (cost + 3) / 4 | 0; }; -TX.prototype.isStandard = function isStandard(flags) { +// IsStandardTx +TX.prototype.isStandard = function isStandard(flags, ts, height, ret) { var i, input, output, type; var nulldata = 0; + if (!ret) + ret = { reason: null }; + if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; - if (this.version > constants.tx.version || this.version < 1) + if (this.version > constants.tx.version || this.version < 1) { + ret.reason = 'version'; return false; + } - if (this.getSize() > constants.tx.maxSize) + if (ts != null) { + if (!tx.isFinal(ts, height)) { + ret.reason = 'non-final'; + return false; + } + } + + if (this.getVirtualSize() > constants.tx.maxSize) { + ret.reason = 'tx-size'; return false; + } for (i = 0; i < this.inputs.length; i++) { input = this.inputs[i]; - if (input.script.getSize() > 1650) + if (input.script.getSize() > 1650) { + ret.reason = 'scriptsig-size'; return false; + } - // Not accurate? + // XXX Not accurate if (this.isCoinbase()) continue; if (flags & constants.flags.VERIFY_SIGPUSHONLY) { - if (!input.script.isPushOnly()) + if (!input.script.isPushOnly()) { + ret.reason = 'scriptsig-not-pushonly'; return false; + } } } @@ -710,32 +742,38 @@ TX.prototype.isStandard = function isStandard(flags) { output = this.outputs[i]; type = output.script.getType(); - if (!output.script.isStandard()) - return false; - - if (type === 'unknown') + if (!output.script.isStandard()) { + ret.reason = 'scriptpubkey'; return false; + } if (type === 'nulldata') { nulldata++; continue; } - if (type === 'multisig' && !constants.tx.bareMultisig) + if (type === 'multisig' && !constants.tx.bareMultisig) { + ret.reason = 'bare-multisig'; return false; + } - if (output.value.cmpn(constants.tx.dustThreshold) < 0) + if (output.value.cmpn(constants.tx.dustThreshold) < 0) { + ret.reason = 'dust'; return false; + } } - if (nulldata > 1) + if (nulldata > 1) { + ret.reason = 'multi-op-return'; return false; + } return true; }; +// AreInputsStandard TX.prototype.isStandardInputs = function isStandardInputs(flags) { - var i, input, args, stack, res, redeem, targs; + var i, input, args, stack, res, redeem, targs, hadWitness; var maxSigops = constants.script.maxScripthashSigops; if (flags == null) @@ -755,21 +793,32 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { if (args < 0) return false; - stack = []; + stack = new bcoin.script.stack([]); - // Bitcoind doesn't do this, but it's possible someone - // could DoS us by sending ridiculous txs to the mempool - // if we don't put this here. + // XXX Not accurate: + // Failsafe to avoid getting dos'd in case we ever + // call isStandardInputs before isStandard. if (!input.script.isPushOnly()) return false; - res = input.script.execute(stack, this, i, flags); - - // TODO: Segwit here. + res = input.script.execute(stack, this, i, flags, 0); if (!res) return false; + if ((flags & constants.flags.VERIFY_WITNESS) + && input.output.isWitnessProgram()) { + hadWitness = true; + + // Input script must be empty. + if (input.script.code.length !== 0) + return false; + + // Verify the program in the output script + if (!this.isStandardProgram(input.witness, input.output.script, flags)) + return false; + } + if ((flags & constants.flags.VERIFY_P2SH) && input.output.script.isScripthash()) { if (stack.length === 0) @@ -795,8 +844,26 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { if (targs < 0) return false; args += targs; + + if ((flags & constants.flags.VERIFY_WITNESS) + && redeem.isWitnessProgram()) { + hasWitness = true; + + // Input script must be exactly one push of the redeem script. + if (!(input.script.code.length === 1 + && utils.isEqual(input.script.code[0], raw))) { + return false; + } + + // Verify the program in the redeem script + if (!this.isStandardProgram(input.witness, redeem, flags)) + return false; + } } + if (hadWitness) + continue; + if (stack.length !== args) return false; } @@ -804,8 +871,43 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { return true; }; +TX.prototype.isStandardProgram = function isStandardProgram(witness, output, flags) { + var program, witnessScript, j; + + assert((flags & constants.flags.VERIFY_WITNESS) !== 0); + assert(output.isWitnessProgram()); + + program = output.getWitnessProgram(); + + if (!program.type) + return false; + + if (program.version > 0) { + if (flags & constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) + return false; + return true; + } + + if (program.type === 'witnesspubkeyhash') { + if (witness.items.length !== 2) + return false; + } else if (program.type === 'witnessscripthash') { + if (witness.items.length === 0) + return false; + } else { + assert(false); + } + + for (j = 0; j < witness.items.length; j++) { + if (witness.items[j].length > constants.script.maxSize) + return false; + } + + return true; +}; + TX.prototype.maxSize = function maxSize() { - return this.getSize(); + return this.getVirtualSize(); }; TX.prototype.getPriority = function getPriority(size) { @@ -858,6 +960,22 @@ TX.prototype.isFree = function isFree(size) { return priority.cmp(constants.tx.freeThreshold) > 0; }; +TX.prototype.getMinFee = function getMinFee(allowFree, size) { + var fee; + + size = size || this.maxSize(); + + if (allowFree && this.isFree(size)) + return new bn(0); + + fee = constants.tx.minFee.muln(size).divn(1000); + + if (fee.cmpn(0) === 0 && constants.tx.minFee.cmpn(0) > 0) + fee = constants.tx.minFee.clone(); + + return fee; +}; + TX.prototype.getHeight = function getHeight() { if (this.height !== -1) return this.height; diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 23334db0..71d24bfb 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -30,6 +30,9 @@ function TXPool(prefix, db, options) { this.options = options; this.busy = false; this.jobs = []; + + if (this.options.mapAddress) + this.options.indexAddress = true; } utils.inherits(TXPool, EventEmitter); @@ -72,10 +75,14 @@ TXPool.prototype._lock = function _lock(func, args, force) { }; TXPool.prototype.getMap = function getMap(tx, callback) { - var input = tx.getInputAddresses(); - var output = tx.getOutputAddresses(); - var addresses = utils.uniqs(input.concat(output)); - var map; + var input, output, addresses, map; + + if (!this.options.indexAddress) + return callback(); + + input = tx.getInputAddresses(); + output = tx.getOutputAddresses(); + addresses = utils.uniqs(input.concat(output)); function cb(err, map) { if (err) @@ -102,7 +109,7 @@ TXPool.prototype.getMap = function getMap(tx, callback) { return callback(null, map); } - if (!this.options.ids) { + if (!this.options.mapAddress) { map = addresses.reduce(function(out, address) { out[address] = [address]; return out; @@ -249,8 +256,10 @@ TXPool.prototype.add = function add(tx, callback) { if (err) return callback(err); - if (map.all.length === 0) - return callback(null, false); + if (self.options.mapAddress) { + if (map.all.length === 0) + return callback(null, false); + } return self._add(tx, map, callback); }); @@ -291,28 +300,32 @@ TXPool.prototype._add = function add(tx, map, callback, force) { batch.put(prefix + 't/t/' + hash, tx.toExtended()); - if (tx.ts === 0) { - assert(tx.ps > 0); - batch.put(prefix + 't/p/t/' + hash, DUMMY); - batch.put(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash, DUMMY); - } else { - batch.put(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash, DUMMY); - } - - map.all.forEach(function(id) { - batch.put(prefix + 't/a/' + id + '/' + hash, DUMMY); + if (self.options.indexExtra) { if (tx.ts === 0) { - batch.put(prefix + 't/p/a/' + id + '/' + hash, DUMMY); - batch.put( - prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + assert(tx.ps > 0); + batch.put(prefix + 't/p/t/' + hash, DUMMY); + batch.put(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash, DUMMY); } else { - batch.put( - prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put( - prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); + batch.put(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.put(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash, DUMMY); } - }); + + if (self.options.indexAddress) { + map.all.forEach(function(id) { + batch.put(prefix + 't/a/' + id + '/' + hash, DUMMY); + if (tx.ts === 0) { + batch.put(prefix + 't/p/a/' + id + '/' + hash, DUMMY); + batch.put( + prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + } else { + batch.put( + prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.put( + prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); + } + }); + } + } // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { @@ -344,7 +357,7 @@ TXPool.prototype._add = function add(tx, map, callback, force) { updated = true; - if (address) { + if (self.options.indexAddress && address) { map[address].forEach(function(id) { batch.del( prefix + 'u/a/' + id @@ -358,14 +371,24 @@ TXPool.prototype._add = function add(tx, map, callback, force) { + input.prevout.hash + '/' + input.prevout.index); + if (self.options.indexSpent) { + batch.put( + prefix + 's/t/' + + input.prevout.hash + + '/' + input.prevout.index, + DUMMY); + } + return next(); } // Only add orphans if this input is ours. - if (!address || !map[address].length) - return next(); + if (self.options.mapAddress) { + if (!address || !map[address].length) + return next(); + } - self.getTX(input.prevout.hash, function(err, result) { + self.isSpent(input.prevout.hash, input.prevout.index, function(err, result) { if (err) return done(err); @@ -394,8 +417,10 @@ TXPool.prototype._add = function add(tx, map, callback, force) { var key, coin; // Do not add unspents for outputs that aren't ours. - if (!address || !map[address].length) - return next(); + if (self.options.mapAddress) { + if (!address || !map[address].length) + return next(); + } key = hash + '/' + i; coin = bcoin.coin(tx, i); @@ -450,7 +475,7 @@ TXPool.prototype._add = function add(tx, map, callback, force) { return next(err); if (!orphans) { - if (address) { + if (self.options.indexAddress && address) { map[address].forEach(function(id) { batch.put( prefix + 'u/a/' + id @@ -490,6 +515,39 @@ TXPool.prototype._add = function add(tx, map, callback, force) { }); }; +TXPool.prototype.isSpent = function isSpent(hash, index, callback, checkCoin) { + var self = this; + + if (this.options.indexSpent) { + return this.db.get('s/t/' + hash + '/' + index, function(err, exists) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + return callback(null, !!exists); + }); + } + + function getCoin(callback) { + if (!checkCoin) + return callback(null, null); + return self.getCoin(hash, index, callback); + } + + return getCoin(function(err, coin) { + if (err) + return callback(err); + + if (coin) + return callback(null, false); + + return self.getTX(hash, function(err, tx) { + if (err) + return callback(err); + return callback(null, !!tx); + }); + }); +}; + TXPool.prototype._confirm = function _confirm(tx, map, callback) { var self = this; var prefix = this.prefix + '/'; @@ -520,24 +578,31 @@ TXPool.prototype._confirm = function _confirm(tx, map, callback) { assert(existing.ps > 0); batch.put(prefix + 't/t/' + hash, tx.toExtended()); - batch.del(prefix + 't/p/t/' + hash); - batch.put(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.del(prefix + 't/s/s/' + pad32(existing.ps) + '/' + hash); - batch.put(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash, DUMMY); - map.all.forEach(function(id) { - batch.del(prefix + 't/p/a/' + id + '/' + hash); - batch.put(prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.del(prefix + 't/s/a/' + id + '/' + pad32(existing.ps) + '/' + hash); - batch.put(prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); - }); + if (self.options.indexExtra) { + batch.del(prefix + 't/p/t/' + hash); + batch.put(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.del(prefix + 't/s/s/' + pad32(existing.ps) + '/' + hash); + batch.put(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash, DUMMY); + + if (self.options.indexAddress) { + map.all.forEach(function(id) { + batch.del(prefix + 't/p/a/' + id + '/' + hash); + batch.put(prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.del(prefix + 't/s/a/' + id + '/' + pad32(existing.ps) + '/' + hash); + batch.put(prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); + }); + } + } utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getAddress(); // Only update coins if this output is ours. - if (!address || !map[address].length) - return next(); + if (self.options.mapAddress) { + if (!address || !map[address].length) + return next(); + } self.getCoin(hash, i, function(err, coin) { if (err) @@ -571,6 +636,10 @@ TXPool.prototype._confirm = function _confirm(tx, map, callback) { TXPool.prototype.remove = function remove(hash, callback) { var self = this; + var first = Array.isArray(hash) ? hash[0] : hash; + + if (!this.options.indexExtra && (first instanceof bcoin.tx)) + return this.lazyRemove(hash, callback); if (Array.isArray(hash)) { return utils.forEachSerial(hash, function(hash, next) { @@ -594,8 +663,10 @@ TXPool.prototype.remove = function remove(hash, callback) { if (err) return callback(err); - if (map.all.length === 0) - return callback(null, false); + if (self.options.mapAddress) { + if (map.all.length === 0) + return callback(null, false); + } return self._remove(tx, map, callback); }); @@ -615,8 +686,10 @@ TXPool.prototype.lazyRemove = function lazyRemove(tx, callback) { if (err) return callback(err); - if (map.all.length === 0) - return callback(null, false); + if (self.options.mapAddress) { + if (map.all.length === 0) + return callback(null, false); + } return self._remove(tx, map, callback); }); @@ -630,24 +703,28 @@ TXPool.prototype._remove = function remove(tx, map, callback) { batch.del(prefix + 't/t/' + hash); - if (tx.ts === 0) { - batch.del(prefix + 't/p/t/' + hash); - batch.del(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash); - } else { - batch.del(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash); - batch.del(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash); - } - - map.all.forEach(function(id) { - batch.del(prefix + 't/a/' + id + '/' + hash); + if (self.options.indexExtra) { if (tx.ts === 0) { - batch.del(prefix + 't/p/a/' + id + '/' + hash); - batch.del(prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash); + batch.del(prefix + 't/p/t/' + hash); + batch.del(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash); } else { - batch.del(prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash); - batch.del(prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash); + batch.del(prefix + 't/h/h/' + pad32(tx.height) + '/' + hash); + batch.del(prefix + 't/s/s/' + pad32(tx.ts) + '/' + hash); } - }); + + if (self.options.indexAddress) { + map.all.forEach(function(id) { + batch.del(prefix + 't/a/' + id + '/' + hash); + if (tx.ts === 0) { + batch.del(prefix + 't/p/a/' + id + '/' + hash); + batch.del(prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash); + } else { + batch.del(prefix + 't/h/a/' + id + '/' + pad32(tx.height) + '/' + hash); + batch.del(prefix + 't/s/a/' + id + '/' + pad32(tx.ts) + '/' + hash); + } + }); + } + } this.fillTX(tx, function(err) { if (err) @@ -662,33 +739,47 @@ TXPool.prototype._remove = function remove(tx, map, callback) { if (!input.output) return; - if (!address || !map[address].length) - return; + if (self.options.mapAddress) { + if (!address || !map[address].length) + return; + } - map[address].forEach(function(id) { - batch.put(prefix + 'u/a/' + id - + '/' + input.prevout.hash - + '/' + input.prevout.index, - DUMMY); - }); + if (self.options.indexAddress && address) { + map[address].forEach(function(id) { + batch.put(prefix + 'u/a/' + id + + '/' + input.prevout.hash + + '/' + input.prevout.index, + DUMMY); + }); + } batch.put(prefix + 'u/t/' + input.prevout.hash + '/' + input.prevout.index, input.output.toExtended()); + if (self.options.indexSpent) { + batch.del(prefix + 's/t/' + + input.prevout.hash + + '/' + input.prevout.index); + } + batch.del(prefix + 'o/' + input.prevout.hash + '/' + input.prevout.index); }); tx.outputs.forEach(function(output, i) { var address = output.getAddress(); - if (!address || !map[address].length) - return; + if (self.options.mapAddress) { + if (!address || !map[address].length) + return; + } - map[address].forEach(function(id) { - batch.del(prefix + 'u/a/' + id + '/' + hash + '/' + i); - }); + if (self.options.indexAddress && address) { + map[address].forEach(function(id) { + batch.del(prefix + 'u/a/' + id + '/' + hash + '/' + i); + }); + } batch.del(prefix + 'u/t/' + hash + '/' + i); }); @@ -731,8 +822,10 @@ TXPool.prototype.unconfirm = function unconfirm(hash, callback) { if (err) return callback(err); - if (map.all.length === 0) - return callback(null, false); + if (self.options.mapAddress) { + if (map.all.length === 0) + return callback(null, false); + } return self._unconfirm(tx, map, callback); }); @@ -757,17 +850,22 @@ TXPool.prototype._unconfirm = function unconfirm(tx, map, callback) { tx.block = null; batch.put(prefix + 't/t/' + hash, tx.toExtended()); - batch.put(prefix + 't/p/t/' + hash, DUMMY); - batch.del(prefix + 't/h/h/' + pad32(height) + '/' + hash); - batch.del(prefix + 't/s/s/' + pad32(ts) + '/' + hash); - batch.put(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash, DUMMY); - map.all.forEach(function(id) { - batch.put(prefix + 't/p/a/' + id + '/' + hash, DUMMY); - batch.del(prefix + 't/h/a/' + id + '/' + pad32(height) + '/' + hash); - batch.del(prefix + 't/s/a/' + id + '/' + pad32(ts) + '/' + hash); - batch.put(prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); - }); + if (self.options.indexExtra) { + batch.put(prefix + 't/p/t/' + hash, DUMMY); + batch.del(prefix + 't/h/h/' + pad32(height) + '/' + hash); + batch.del(prefix + 't/s/s/' + pad32(ts) + '/' + hash); + batch.put(prefix + 't/s/s/' + pad32(tx.ps) + '/' + hash, DUMMY); + + if (self.options.indexAddress) { + map.all.forEach(function(id) { + batch.put(prefix + 't/p/a/' + id + '/' + hash, DUMMY); + batch.del(prefix + 't/h/a/' + id + '/' + pad32(height) + '/' + hash); + batch.del(prefix + 't/s/a/' + id + '/' + pad32(ts) + '/' + hash); + batch.put(prefix + 't/s/a/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + }); + } + } utils.forEachSerial(tx.outputs, function(output, next, i) { self.getCoin(hash, i, function(err, coin) { @@ -1356,8 +1454,8 @@ TXPool.prototype.getBalanceByAddress = function getBalanceByAddress(address, cal }); }; -TXPool.prototype.getAll = function getAll(callback) { - return this.getAllByAddress(null, callback); +TXPool.prototype.getAllHashes = function getAllHashes(callback) { + return this.getTXHashes(null, callback); }; TXPool.prototype.getCoins = function getCoins(callback) { diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index bf93b717..358abeaa 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -1884,6 +1884,13 @@ utils.serial = function serial(stack, callback) { })(); }; +utils.toMap = function toMap(arr) { + return arr.reduce(function(out, value) { + out[value] = true; + return out; + }, {}); +}; + function SyncBatch(db) { this.db = db; this.ops = []; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index ef4c7d5c..7875acbf 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -98,7 +98,10 @@ WalletDB.prototype._init = function _init() { }); this.tx = new bcoin.txdb('w', this.db, { - ids: true + indexSpent: false, + indexExtra: true, + indexAddress: true, + mapAddress: true }); this.tx.on('error', function(err) { @@ -543,16 +546,16 @@ WalletDB.prototype.fillCoin = function fillCoin(tx, callback) { WalletDB.prototype.removeBlockSPV = function removeBlockSPV(block, callback) { var self = this; + callback = utils.ensure(callback); + this.tx.getHeightHashes(block.height, function(err, txs) { if (err) return callback(err); - txs.forEach(function(tx) { - self.tx.unconfirm(tx); - }); - - callback(); + utils.forEachSerial(txs, function(tx, next) { + self.tx.unconfirm(tx, next); + }, callback); }); }; @@ -576,15 +579,31 @@ function Provider(db) { EventEmitter.call(this); - this.loaded = true; + this.loaded = false; this.db = db; this.id = null; + + this._init(); } utils.inherits(Provider, EventEmitter); +Provider.prototype._init = function _init() { + var self = this; + + if (this.db.loaded) { + this.loaded = true; + return; + } + + this.db.once('open', function() { + self.loaded = true; + self.emit('open'); + }); +}; + Provider.prototype.open = function open(callback) { - return utils.nextTick(callback); + return this.db.open(callback); }; Provider.prototype.setID = function setID(id) { diff --git a/lib/bcoin/workers.js b/lib/bcoin/workers.js index b371f040..f36c0d8c 100644 --- a/lib/bcoin/workers.js +++ b/lib/bcoin/workers.js @@ -161,10 +161,6 @@ workers.listen = function listen() { workers.verify = function verify(tx, index, force, flags) { tx = bcoin.tx.fromExtended(new Buffer(tx, 'hex'), true); - if (tx.getOutputValue().cmp(tx.getInputValue()) > 0) { - utils.debug('TX is spending funds it does not have: %s', tx.rhash); - return false; - } return tx.verify(index, force, flags); };