diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 47390902..2920c3a4 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -73,38 +73,18 @@ TXDB.prototype._lock = function _lock(func, args, force) { TXDB.prototype._loadFilter = function loadFilter(callback) { var self = this; - var iter; if (!this.filter) return callback(); - iter = this.db.iterator({ + this.db.iterate({ gte: 'W', lte: 'W~', - 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); - + transform: function(key) { key = key.split('/')[1]; - self.filter.add(key, 'hex'); - - next(); - }); - })(); + } + }, callback); }; TXDB.prototype._testFilter = function _testFilter(addresses) { @@ -297,12 +277,6 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { TXDB.prototype.add = function add(tx, callback, force) { var self = this; - if (Array.isArray(tx)) { - return utils.forEachSerial(tx, function(tx, next) { - self.add(tx, next, force); - }, callback); - } - return this.getMap(tx, function(err, map) { if (err) return callback(err); @@ -314,18 +288,13 @@ TXDB.prototype.add = function add(tx, callback, force) { }); }; -// This big scary function is what a persistent tx pool -// looks like. It's a semi mempool in that it can handle -// receiving txs out of order. TXDB.prototype._add = function add(tx, map, callback, force) { var self = this; - var hash = tx.hash('hex'); var updated = false; - var batch; + var batch, hash, i, j, unlock, path, paths, id; - assert(tx.ts > 0 || tx.ps > 0); + unlock = this._lock(add, [tx, map, callback], force); - var unlock = this._lock(add, [tx, map, callback], force); if (!unlock) return; @@ -341,14 +310,15 @@ TXDB.prototype._add = function add(tx, map, callback, force) { // Ignore if we already have this tx. if (existing) - return callback(null, true); + return callback(null, true, map); + + hash = tx.hash('hex'); batch = self.db.batch(); batch.put('t/' + hash, tx.toExtended()); if (tx.ts === 0) { - assert(tx.ps > 0); batch.put('p/' + hash, DUMMY); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); } else { @@ -356,8 +326,9 @@ TXDB.prototype._add = function add(tx, map, callback, force) { batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); } - map.all.forEach(function(path) { - var id = path.id + '/' + path.account; + for (i = 0; i < map.all.length; i++) { + path = map.all[i]; + id = path.id + '/' + path.account; batch.put('T/' + id + '/' + hash, DUMMY); if (tx.ts === 0) { batch.put('P/' + id + '/' + hash, DUMMY); @@ -366,12 +337,12 @@ TXDB.prototype._add = function add(tx, map, callback, force) { batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); } - }); + } // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { - var key, address; var prevout = input.prevout; + var key, address; if (tx.isCoinbase()) return next(); @@ -388,6 +359,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) { key = prevout.hash + '/' + prevout.index; + batch.put('s/' + key, tx.hash()); + if (coin) { // Add TX to inputs and spend money input.coin = coin; @@ -400,28 +373,28 @@ TXDB.prototype._add = function add(tx, map, callback, force) { updated = true; - if (address) { - map.table[address].forEach(function(path) { - var id = path.id + '/' + path.account; - batch.del('C/' + id + '/' + key); - }); + paths = map.table[address]; + + for (j = 0; j < paths.length; j++) { + path = paths[j]; + id = path.id + '/' + path.account; + batch.del('C/' + id + '/' + key); } batch.del('c/' + key); - batch.put('s/' + key, tx.hash()); return next(); } input.coin = null; - self.isSpent(prevout.hash, prevout.index, function(err, spentBy) { + self.isSpent(prevout.hash, prevout.index, function(err, spent) { if (err) return next(err); // Are we double-spending? // Replace older txs with newer ones. - if (spentBy) { + if (spent) { return self.getTX(prevout.hash, function(err, prev) { if (err) return next(err); @@ -434,19 +407,22 @@ TXDB.prototype._add = function add(tx, map, callback, force) { // Skip invalid transactions if (self.options.verify) { if (!tx.verify(i)) - return callback(null, false); + return callback(null, false, map); } - return self._removeSpenders(spentBy, tx, function(err, result) { + return self._removeConflict(spent, tx, function(err, rtx, rmap) { if (err) return next(err); - if (!result) { - assert(tx.ts === 0, 'I\'m confused'); - return callback(null, false); - } + // Spender was not removed, the current + // transaction is not elligible to be added. + if (!rtx) + return callback(null, false, map); + + self.emit('conflict', rtx, rmap); batch.clear(); + self._add(tx, map, callback, true); }); }); @@ -470,14 +446,13 @@ TXDB.prototype._add = function add(tx, map, callback, force) { // Add unspent outputs or resolve orphans utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getHash(); - var key, coin; + var key = hash + '/' + i; + var coin; // Do not add unspents for outputs that aren't ours. if (!address || !map.table[address].length) return next(); - key = hash + '/' + i; - if (output.script.isUnspendable()) return next(); @@ -518,11 +493,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(); } - self.lazyRemove(orphan.tx, function(err) { - if (err) - return next(err); - return next(); - }, true); + self.lazyRemove(orphan.tx, next, true); }, function(err) { if (err) return next(err); @@ -538,11 +509,12 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(err); if (!orphans) { - if (address) { - map.table[address].forEach(function(path) { - var id = path.id + '/' + path.account; - batch.put('C/' + id + '/' + key, DUMMY); - }); + paths = map.table[address]; + + for (j = 0; j < paths.length; j++) { + path = paths[j]; + id = path.id + '/' + path.account; + batch.put('C/' + id + '/' + key, DUMMY); } batch.put('c/' + key, coin.toRaw()); @@ -573,7 +545,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { self.emit('updated', tx, map); } - return callback(null, true); + return callback(null, true, map); }); }); }); @@ -593,8 +565,9 @@ TXDB.prototype._add = function add(tx, map, callback, force) { * @param {Function} callback - Returns [Error, Boolean]. */ -TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) { +TXDB.prototype._removeConflict = function _removeConflict(hash, ref, callback) { var self = this; + this.getTX(hash, function(err, tx) { if (err) return callback(err); @@ -602,32 +575,77 @@ TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) { if (!tx) return callback(new Error('Could not find spender.')); - if (tx.ts !== 0) - return callback(null, false); + if (tx.ts !== 0) { + // If spender is confirmed and replacement + // is not confirmed, do nothing. + if (ref.ts === 0) + return callback(); - if (ref.ts === 0 && ref.ps < tx.ps) - return callback(null, false); + // If both are confirmed but replacement + // is older than spender, do nothing. + if (ref.ts < tx.ts) + return callback(); + } else { + // If spender is unconfirmed and replacement + // is confirmed, do nothing. + if (ref.ts !== 0) + return callback(); - utils.forEachSerial(tx.outputs, function(output, next, i) { - self.isSpent(hash, i, function(err, spent) { - if (err) - return next(err); - if (spent) - return self._removeSpenders(spent, ref, next); - next(); - }); - }, function(err) { + // If both are unconfirmed but replacement + // is older than spender, do nothing. + if (ref.ps < tx.ps) + return callback(); + } + + self._removeRecursive(tx, function(err, result, map) { if (err) return callback(err); - return self.lazyRemove(tx, function(err) { - if (err) - return callback(err); - return callback(null, true); - }, true); + return callback(null, tx, map); }); }); }; +/** + * Remove a transaction and recursively + * remove all of its spenders. + * @private + * @param {TX} tx - Transaction to be removed. + * @param {Function} callback - Returns [Error, Boolean]. + */ + +TXDB.prototype._removeRecursive = function _removeRecursive(tx, callback) { + var self = this; + var hash = tx.hash('hex'); + + utils.forEachSerial(tx.outputs, function(output, next, i) { + self.isSpent(hash, i, function(err, spent) { + if (err) + return next(err); + + // Remove all of the spender's spenders first. + if (spent) { + return self.getTX(spent, function(err, tx) { + if (err) + return callback(err); + + if (!tx) + return callback(new Error('Could not find spender.')); + + return self._removeRecursive(tx, next); + }); + } + + next(); + }); + }, function(err) { + if (err) + return callback(err); + + // Remove the spender. + return self.lazyRemove(tx, callback, true); + }); +}; + /** * Test an entire transaction to see * if any of its outpoints are a double-spend. @@ -637,6 +655,7 @@ TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) { TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) { var self = this; + utils.everySerial(tx.inputs, function(input, next) { self.isSpent(input.prevout.hash, input.prevout.index, function(err, spent) { if (err) @@ -676,30 +695,33 @@ TXDB.prototype.isSpent = function isSpent(hash, index, callback) { TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { var self = this; - var hash = tx.hash('hex'); - var batch; + var hash, batch, unlock, i, path, id; + + unlock = this._lock(_confirm, [tx, map, callback], force); - var unlock = this._lock(_confirm, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); + hash = tx.hash('hex'); + this.getTX(hash, function(err, existing) { if (err) return callback(err); // Haven't seen this tx before, add it. if (!existing) - return callback(null, false); + return callback(null, false, map); // Existing tx is already confirmed. Ignore. if (existing.ts !== 0) - return callback(null, true); + return callback(null, true, map); - // The incoming tx won't confirm the existing one anyway. Ignore. + // The incoming tx won't confirm the + // existing one anyway. Ignore. if (tx.ts === 0) - return callback(null, true); + return callback(null, true, map); batch = self.db.batch(); @@ -715,13 +737,14 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { batch.del('m/' + pad32(existing.ps) + '/' + hash); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); - map.all.forEach(function(path) { - var id = path.id + '/' + path.account; + for (i = 0; i < map.all.length; i++) { + path = map.all[i]; + id = path.id + '/' + path.account; batch.del('P/' + id + '/' + hash); batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); - }); + } utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getHash(); @@ -758,7 +781,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { self.emit('confirmed', tx, map); self.emit('tx', tx, map); - return callback(null, true); + return callback(null, true, map); }); }); }); @@ -774,12 +797,6 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { TXDB.prototype.remove = function remove(hash, callback, force) { var self = this; - if (Array.isArray(hash)) { - return utils.forEachSerial(hash, function(hash, next) { - self.remove(hash, next, force); - }, callback); - } - if (hash.hash) hash = hash.hash('hex'); @@ -815,12 +832,6 @@ TXDB.prototype.remove = function remove(hash, callback, force) { TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { var self = this; - if (Array.isArray(tx)) { - return utils.forEachSerial(tx, function(tx, next) { - self.lazyRemove(tx, next, force); - }, callback); - } - return this.getMap(tx, function(err, map) { if (err) return callback(err); @@ -842,15 +853,17 @@ TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { TXDB.prototype._remove = function remove(tx, map, callback, force) { var self = this; - var hash = tx.hash('hex'); - var batch; + var unlock, hash, batch, i, j, path, id, key, paths, address, input, output; + + unlock = this._lock(remove, [tx, map, callback], force); - var unlock = this._lock(remove, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); + hash = tx.hash('hex'); + batch = this.db.batch(); batch.del('t/' + hash); @@ -863,8 +876,9 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { batch.del('m/' + pad32(tx.ts) + '/' + hash); } - map.all.forEach(function(path) { - var id = path.id + '/' + path.account; + for (i = 0; i < map.all.length; i++) { + path = map.all[i]; + id = path.id + '/' + path.account; batch.del('T/' + id + '/' + hash); if (tx.ts === 0) { batch.del('P/' + id + '/' + hash); @@ -873,56 +887,60 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash); } - }); + } this.fillHistory(tx, function(err) { if (err) return callback(err); - tx.inputs.forEach(function(input) { - var key = input.prevout.hash + '/' + input.prevout.index; - var address = input.getHash(); + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + key = input.prevout.hash + '/' + input.prevout.index; + address = input.getHash(); if (tx.isCoinbase()) - return; + break; if (!input.coin) - return; + continue; if (!address || !map.table[address].length) - return; + continue; - if (address) { - map.table[address].forEach(function(path) { - var id = path.id + '/' + path.account; - batch.put('C/' + id + '/' + key, DUMMY); - }); + paths = map.table[address]; + + for (j = 0; j < paths.length; j++) { + path = paths[j]; + id = path.id + '/' + path.account; + batch.put('C/' + id + '/' + key, DUMMY); } batch.put('c/' + key, input.coin.toRaw()); batch.del('s/' + key); batch.del('o/' + key); - }); + } - tx.outputs.forEach(function(output, i) { - var key = hash + '/' + i; - var address = output.getHash(); + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + key = hash + '/' + i; + address = output.getHash(); if (!address || !map.table[address].length) - return; + continue; if (output.script.isUnspendable()) - return; + continue; - if (address) { - map.table[address].forEach(function(path) { - var id = path.id + '/' + path.account; - batch.del('C/' + id + '/' + key); - }); + paths = map.table[address]; + + for (j = 0; j < paths.length; j++) { + path = paths[j]; + id = path.id + '/' + path.account; + batch.del('C/' + id + '/' + key); } batch.del('c/' + key); - }); + } batch.write(function(err) { if (err) @@ -930,7 +948,7 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { self.emit('remove tx', tx, map); - return callback(null, true); + return callback(null, true, map); }); }); }; @@ -944,12 +962,6 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { var self = this; - if (Array.isArray(hash)) { - return utils.forEachSerial(hash, function(hash, next) { - self.unconfirm(hash, next, force); - }, callback); - } - if (hash.hash) hash = hash.hash('hex'); @@ -985,21 +997,23 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { var self = this; - var hash, batch, height, ts; + var batch, unlock, hash, height, ts, i, path, id; + + unlock = this._lock(unconfirm, [tx, map, callback], force); - var unlock = this._lock(unconfirm, [tx, map, callback], force); if (!unlock) return; callback = utils.wrap(callback, unlock); hash = tx.hash('hex'); - batch = this.db.batch(); height = tx.height; ts = tx.ts; + batch = this.db.batch(); + if (height !== -1) - return callback(null, false); + return callback(null, false, map); tx.height = -1; tx.ps = utils.now(); @@ -1014,13 +1028,14 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { batch.del('m/' + pad32(ts) + '/' + hash); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - map.all.forEach(function(path) { - var id = path.id + '/' + path.account; + for (i = 0; i < map.all.length; i++) { + path = map.all[i]; + id = path.id + '/' + path.account; batch.put('P/' + id + '/' + hash, DUMMY); batch.del('H/' + id + '/' + pad32(height) + '/' + hash); batch.del('M/' + id + '/' + pad32(ts) + '/' + hash); batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); - }); + } utils.forEachSerial(tx.outputs, function(output, next, i) { self.getCoin(hash, i, function(err, coin) { @@ -1046,7 +1061,7 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { self.emit('unconfirmed', tx, map); - return callback(null, true); + return callback(null, true, map); }); }); }; @@ -1607,6 +1622,7 @@ TXDB.prototype.getBalance = function getBalance(id, callback) { TXDB.prototype.zap = function zap(id, age, callback, force) { var self = this; + var unlock; if (typeof age === 'function') { force = callback; @@ -1615,7 +1631,8 @@ TXDB.prototype.zap = function zap(id, age, callback, force) { id = null; } - var unlock = this._lock(zap, [id, age, callback], force); + unlock = this._lock(zap, [id, age, callback], force); + if (!unlock) return; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 20bfc26c..631344a0 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2243,10 +2243,10 @@ utils.pad32 = function pad32(num) { */ utils.wrap = function wrap(callback, unlock) { - return function(err, result) { + return function(err, res1, res2) { unlock(); if (callback) - callback(err, result); + callback(err, res1, res2); }; }; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 1eb36b92..89eed7f7 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -202,6 +202,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) { } unlock = this.locker.lock(addKey, [account, key, callback]); + if (!unlock) return; @@ -235,6 +236,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) { } unlock = this.locker.lock(removeKey, [account, key, callback]); + if (!unlock) return; @@ -339,6 +341,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force var master, key, unlock; unlock = this.locker.lock(createAccount, [options, callback], force); + if (!unlock) return; @@ -396,6 +399,7 @@ Wallet.prototype.getAccounts = function getAccounts(callback) { Wallet.prototype.getAccount = function getAccount(account, callback, force) { var unlock = this.locker.lock(getAccount, [account, callback], force); + if (!unlock) return; @@ -454,6 +458,7 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac } unlock = this.locker.lock(createAddress, [account, change, callback]); + if (!unlock) return; @@ -790,6 +795,7 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { var i, path, unlock; unlock = this.locker.lock(syncOutputDepth, [tx, callback]); + if (!unlock) return; @@ -911,6 +917,7 @@ Wallet.prototype.scan = function scan(getByAddress, callback) { var unlock; unlock = this.locker.lock(scan, [getByAddress, callback]); + if (!unlock) return; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 74d19dd9..0262071e 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -128,6 +128,13 @@ WalletDB.prototype._init = function _init() { }); }); + this.tx.on('conflict', function(tx, map) { + self.emit('conflict', tx, map); + map.all.forEach(function(path) { + self.fire(path.id, 'conflict', tx, path.name); + }); + }); + this.tx.on('confirmed', function(tx, map) { self.emit('confirmed', tx, map); map.all.forEach(function(path) { diff --git a/test/wallet-test.js b/test/wallet-test.js index 99fb076e..ed01c86b 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -192,7 +192,6 @@ describe('Wallet', function() { var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); t1.addInput(dummyInput); // balance: 51000 - // w.sign(t1); w.sign(t1, function(err) { assert.ifError(err); var t2 = bcoin.mtx().addInput(t1, 0) // 50000 @@ -200,14 +199,12 @@ describe('Wallet', function() { .addOutput(w, 24000); di = t2.inputs[0]; // balance: 49000 - // w.sign(t2); w.sign(t2, function(err) { assert.ifError(err); var t3 = bcoin.mtx().addInput(t1, 1) // 1000 .addInput(t2, 0) // 24000 .addOutput(w, 23000); // balance: 47000 - // w.sign(t3); w.sign(t3, function(err) { assert.ifError(err); var t4 = bcoin.mtx().addInput(t2, 1) // 24000 @@ -215,19 +212,16 @@ describe('Wallet', function() { .addOutput(w, 11000) .addOutput(w, 11000); // balance: 22000 - // w.sign(t4); w.sign(t4, function(err) { assert.ifError(err); var f1 = bcoin.mtx().addInput(t4, 1) // 11000 .addOutput(f, 10000); // balance: 11000 - // w.sign(f1); w.sign(f1, function(err) { assert.ifError(err); var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) .addOutput(w, 500); // Script inputs but do not sign - // w.scriptInputs(fake); w.scriptInputs(fake, function(err) { assert.ifError(err); // Fake signature @@ -300,13 +294,36 @@ describe('Wallet', function() { it('should cleanup spenders after double-spend', function(cb) { var t1 = bcoin.mtx().addOutput(dw, 5000); - t1.addInput(di); - walletdb.addTX(t1, function(err) { + t1.addInput(di.coin); + dw.getHistory(function(err, txs) { assert.ifError(err); - dw.getBalance(function(err, balance) { + assert.equal(txs.length, 5); + var total = txs.reduce(function(t, tx) { + return t + tx.getOutputValue(); + }, 0); + assert.equal(total, 154000); + dw.sign(t1, function(err) { assert.ifError(err); - assert.equal(balance.total, 11000); - cb(); + dw.getBalance(function(err, balance) { + assert.ifError(err); + assert.equal(balance.total, 11000); + walletdb.addTX(t1, function(err) { + assert.ifError(err); + dw.getBalance(function(err, balance) { + assert.ifError(err); + assert.equal(balance.total, 6000); + dw.getHistory(function(err, txs) { + assert.ifError(err); + assert.equal(txs.length, 2); + var total = txs.reduce(function(t, tx) { + return t + tx.getOutputValue(); + }, 0); + assert.equal(total, 56000); + cb(); + }); + }); + }); + }); }); }); });