From 3412916c89d15b05637b476929967c3ccc9e38b2 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 18 Oct 2016 17:58:54 -0700 Subject: [PATCH] txdb: refactor. --- lib/http/server.js | 21 +- lib/wallet/txdb.js | 980 ++++++++++++++++++++--------------------- lib/wallet/wallet.js | 1 - lib/wallet/walletdb.js | 10 +- test/chain-test.js | 1 + 5 files changed, 504 insertions(+), 509 deletions(-) diff --git a/lib/http/server.js b/lib/http/server.js index 7d483a52..890695c9 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -1260,26 +1260,31 @@ HTTPServer.prototype._initIO = function _initIO() { }); this.walletdb.on('tx', function(id, tx, details) { - self.server.io.to(id).emit('wallet tx', details); - self.server.io.to('!all').emit('wallet tx', id, details); + var json = details.toJSON(); + self.server.io.to(id).emit('wallet tx', json); + self.server.io.to('!all').emit('wallet tx', id, json); }); this.walletdb.on('confirmed', function(id, tx, details) { - self.server.io.to(id).emit('wallet confirmed', details); - self.server.io.to('!all').emit('wallet confirmed', id, details); + var json = details.toJSON(); + self.server.io.to(id).emit('wallet confirmed', json); + self.server.io.to('!all').emit('wallet confirmed', id, json); }); this.walletdb.on('unconfirmed', function(id, tx, details) { - self.server.io.to(id).emit('wallet unconfirmed', details); - self.server.io.to('!all').emit('wallet unconfirmed', id, details); + var json = details.toJSON(); + self.server.io.to(id).emit('wallet unconfirmed', json); + self.server.io.to('!all').emit('wallet unconfirmed', id, json); }); this.walletdb.on('conflict', function(id, tx, details) { - self.server.io.to(id).emit('wallet conflict', details); - self.server.io.to('!all').emit('wallet conflict', id, details); + var json = details.toJSON(); + self.server.io.to(id).emit('wallet conflict', json); + self.server.io.to('!all').emit('wallet conflict', id, json); }); this.walletdb.on('balance', function(id, balance) { + var json = balance.toJSON(); self.server.io.to(id).emit('wallet balance', json); self.server.io.to('!all').emit('wallet balance', id, json); }); diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index fb5dc6fb..3f0b3d00 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -503,9 +503,8 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { coin = yield this.getSpentCoin(spent, prevout); assert(coin); - input.coin = coin; - if (this.options.verify && tx.height === -1) { + input.coin = coin; if (!(yield tx.verifyInputAsync(i))) return false; } @@ -516,9 +515,8 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { coin = yield this.getCoin(prevout.hash, prevout.index); if (coin) { - input.coin = coin; - if (this.options.verify && tx.height === -1) { + input.coin = coin; if (!(yield tx.verifyInputAsync(i))) return false; } @@ -602,13 +600,14 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { valid = true; input = orphan.tx.inputs[orphan.index]; - input.coin = coin; assert(input.prevout.hash === hash); assert(input.prevout.index === i); - if (this.options.verify && orphan.tx.height === -1) + if (this.options.verify && orphan.tx.height === -1) { + input.coin = coin; valid = yield orphan.tx.verifyInputAsync(orphan.index); + } if (valid) { if (--this.count[orphan.hash] === 0) { @@ -626,294 +625,60 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { }); /** - * Retrieve coins for own inputs, remove - * double spenders, and verify inputs. - * @private + * Save credit. + * @param {Credit} credit + * @param {Path} path + */ + +TXDB.prototype.saveCredit = function saveCredit(credit, path) { + var prevout = credit.coin; + var key = prevout.hash + prevout.index; + var raw = credit.toRaw(); + this.put(layout.c(prevout.hash, prevout.index), raw); + this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); + this.coinCache.set(key, raw); +}; + +/** + * Remove credit. + * @param {Credit} credit + * @param {Path} path + */ + +TXDB.prototype.removeCredit = function removeCredit(credit, path) { + var prevout = credit.coin; + var key = prevout.hash + prevout.index; + this.del(layout.c(prevout.hash, prevout.index)); + this.del(layout.C(path.account, prevout.hash, prevout.index)); + this.coinCache.remove(key); +}; + +/** + * Spend credit. + * @param {Credit} credit * @param {TX} tx - * @returns {Promise} + * @param {Number} i */ -TXDB.prototype.removeConflicts = co(function* removeConflicts(tx) { - var hash = tx.hash('hex'); - var i, input, prevout, spent; - - if (tx.isCoinbase()) - return; - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - prevout = input.prevout; - - // Is it already spent? - spent = yield this.getSpent(prevout.hash, prevout.index); - - if (!spent) - continue; - - // Did _we_ spend it? - if (spent.hash === hash) - continue; - - // Remove the double spender. - yield this.removeConflict(spent.hash, tx); - } -}); +TXDB.prototype.spendCredit = function spendCredit(credit, tx, i) { + var prevout = tx.inputs[i].prevout; + var spender = Outpoint.fromTX(tx, i); + this.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); + this.put(layout.d(spender.hash, spender.index), credit.coin.toRaw()); +}; /** - * Add transaction, runs `confirm()` and `verify()`. + * Unspend credit. * @param {TX} tx - * @returns {Promise} + * @param {Number} i */ -TXDB.prototype.add = co(function* add(tx) { - var result; - - this.start(); - - try { - result = yield this._add(tx); - } catch (e) { - this.drop(); - throw e; - } - - yield this.commit(); - - return result; -}); - -/** - * Add transaction without a lock. - * @private - * @param {TX} tx - * @returns {Promise} - */ - -TXDB.prototype._add = co(function* add(tx) { - var hash = tx.hash('hex'); - var path, account, existing; - var i, input, output, coin; - var prevout, key, spender, raw, credit, details; - - assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); - - existing = yield this.getTX(hash); - - if (existing) { - // Existing tx is already confirmed. Ignore. - if (existing.height !== -1) - return; - - // The incoming tx won't confirm the - // existing one anyway. Ignore. - if (tx.height === -1) - return; - - // Attempt to confirm tx before adding it. - return yield this.confirm(tx, existing); - } - - if (tx.height === -1) { - // We ignore double spends from the mempool. - if (yield this.isDoubleSpend(tx)) - return; - - // We ignore any unconfirmed txs - // that are replace-by-fee. - if (yield this.isRBF(tx)) { - this.put(layout.r(hash), DUMMY); - return; - } - } else { - // This potentially removes double-spenders. - yield this.removeConflicts(tx); - - // Delete the replace-by-fee record. - this.del(layout.r(hash)); - } - - details = new Details(this, tx); - - this.put(layout.t(hash), tx.toExtended()); - - if (tx.height === -1) - this.put(layout.p(hash), DUMMY); - else - this.put(layout.h(tx.height, hash), DUMMY); - - this.put(layout.m(tx.ps, hash), DUMMY); - - // Consume unspent money or add orphans - if (!tx.isCoinbase()) { - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - prevout = input.prevout; - credit = yield this.getCredit(prevout.hash, prevout.index); - - // Only bother if this input is ours. - if (!credit) - continue; - - coin = credit.coin; - path = yield this.getPath(coin); - assert(path); - - details.addInput(i, path, coin); - - key = prevout.hash + prevout.index; - - spender = Outpoint.fromTX(tx, i).toRaw(); - - this.put(layout.s(prevout.hash, prevout.index), spender); - - this.pending.unconfirmed -= coin.value; - - if (tx.height === -1) { - credit.spent = true; - raw = credit.toRaw(); - this.put(layout.c(prevout.hash, prevout.index), raw); - this.coinCache.set(key, raw); - } else { - this.pending.confirmed -= coin.value; - this.del(layout.c(prevout.hash, prevout.index)); - this.del(layout.C(path.account, prevout.hash, prevout.index)); - this.coinCache.remove(key); - this.pending.coin--; - } - - this.put(layout.d(hash, i), coin.toRaw()); - } - } - - // Add unspent outputs or resolve orphans. - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - path = yield this.getPath(output); - key = hash + i; - - // Do not add unspents for - // outputs that aren't ours. - if (!path) - continue; - - details.addOutput(i, path); - - credit = Credit.fromTX(tx, i); - coin = credit.coin; - raw = credit.toRaw(); - - this.pending.unconfirmed += coin.value; - - if (tx.height !== -1) - this.pending.confirmed += coin.value; - - this.put(layout.c(hash, i), raw); - this.put(layout.C(path.account, hash, i), DUMMY); - this.pending.coin++; - - this.coinCache.set(key, raw); - } - - for (i = 0; i < details.accounts.length; i++) { - account = details.accounts[i]; - - this.put(layout.T(account, hash), DUMMY); - - if (tx.height === -1) - this.put(layout.P(account, hash), DUMMY); - else - this.put(layout.H(account, tx.height, hash), DUMMY); - - this.put(layout.M(account, tx.ps, hash), DUMMY); - } - - this.pending.tx++; - this.put(layout.R, this.pending.commit()); - - // Clear any locked coins to free up memory. - this.unlockTX(tx); - - this.emit('tx', tx, details); - - if (tx.height !== -1) - this.emit('confirmed', tx, details); - - this.emit('balance', this.pending.toBalance(), details); - - return details; -}); - -/** - * Remove spenders that have not been confirmed. We do this in the - * odd case of stuck transactions or when a coin is double-spent - * by a newer transaction. All previously-spending transactions - * of that coin that are _not_ confirmed will be removed from - * the database. - * @private - * @param {Hash} hash - * @param {TX} ref - Reference tx, the tx that double-spent. - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.removeConflict = co(function* removeConflict(hash, ref) { - var tx = yield this.getTX(hash); - var details; - - assert(tx); - - this.logger.warning('Handling conflicting tx: %s.', utils.revHex(hash)); - - this.drop(); - - details = yield this.removeRecursive(tx); - - this.start(); - - this.logger.warning('Removed conflict: %s.', tx.rhash); - - // Emit the _removed_ transaction. - this.emit('conflict', tx, details); - - return details; -}); - -/** - * Remove a transaction and recursively - * remove all of its spenders. - * @private - * @param {TX} tx - Transaction to be removed. - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.removeRecursive = co(function* removeRecursive(tx) { - var hash = tx.hash('hex'); - var i, spent, stx, details; - - for (i = 0; i < tx.outputs.length; i++) { - spent = yield this.getSpent(hash, i); - - if (!spent) - continue; - - // Remove all of the spender's spenders first. - stx = yield this.getTX(spent.hash); - - assert(stx); - - yield this.removeRecursive(stx); - } - - this.start(); - - // Remove the spender. - details = yield this.lazyRemove(tx); - - assert(details); - - yield this.commit(); - - return details; -}); +TXDB.prototype.unspendCredit = function unspendCredit(tx, i) { + var prevout = tx.inputs[i].prevout; + var spender = Outpoint.fromTX(tx, i); + this.del(layout.s(prevout.hash, prevout.index)); + this.del(layout.d(spender.hash, spender.index)); +}; /** * Test an entire transaction to see @@ -987,6 +752,182 @@ TXDB.prototype.isSpent = co(function* isSpent(hash, index) { return data != null; }); +/** + * Add transaction, runs `confirm()` and `verify()`. + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype.add = co(function* add(tx) { + var result; + + this.start(); + + try { + result = yield this._add(tx); + } catch (e) { + this.drop(); + throw e; + } + + yield this.commit(); + + return result; +}); + +/** + * Add transaction without a lock. + * @private + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype._add = co(function* add(tx) { + var hash = tx.hash('hex'); + var existing = yield this.getTX(hash); + + assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); + + if (existing) { + // Existing tx is already confirmed. Ignore. + if (existing.height !== -1) + return; + + // The incoming tx won't confirm the + // existing one anyway. Ignore. + if (tx.height === -1) + return; + + // Save the original time. + tx.ps = existing.ps; + + // Attempt to confirm tx before adding it. + return yield this.confirm(tx); + } + + if (tx.height === -1) { + // We ignore double spends from the mempool. + if (yield this.isDoubleSpend(tx)) + return; + + // We ignore any unconfirmed txs + // that are replace-by-fee. + if (yield this.isRBF(tx)) { + this.put(layout.r(hash), DUMMY); + return; + } + } else { + // Potentially remove double-spenders. + yield this.removeConflicts(tx); + + // Delete the replace-by-fee record. + this.del(layout.r(hash)); + } + + return yield this._insert(tx); +}); + +/** + * Add transaction without a lock. + * @private + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype._insert = co(function* insert(tx) { + var hash = tx.hash('hex'); + var details = new Details(this, tx); + var i, input, output, coin; + var prevout, credit, path, account; + + if (!tx.isCoinbase()) { + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; + credit = yield this.getCredit(prevout.hash, prevout.index); + + if (!credit) + continue; + + coin = credit.coin; + path = yield this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + this.spendCredit(credit, tx, i); + + this.pending.unconfirmed -= coin.value; + + if (tx.height === -1) { + credit.spent = true; + this.saveCredit(credit, path); + } else { + this.pending.coin--; + this.pending.confirmed -= coin.value; + this.removeCredit(credit, path); + } + } + } + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + path = yield this.getPath(output); + + if (!path) + continue; + + details.setOutput(i, path); + + credit = Credit.fromTX(tx, i); + + this.pending.coin++; + + this.pending.unconfirmed += output.value; + + if (tx.height !== -1) + this.pending.confirmed += output.value; + + this.saveCredit(credit, path); + } + + this.put(layout.t(hash), tx.toExtended()); + + if (tx.height === -1) + this.put(layout.p(hash), DUMMY); + else + this.put(layout.h(tx.height, hash), DUMMY); + + this.put(layout.m(tx.ps, hash), DUMMY); + + for (i = 0; i < details.accounts.length; i++) { + account = details.accounts[i]; + + this.put(layout.T(account, hash), DUMMY); + + if (tx.height === -1) + this.put(layout.P(account, hash), DUMMY); + else + this.put(layout.H(account, tx.height, hash), DUMMY); + + this.put(layout.M(account, tx.ps, hash), DUMMY); + } + + this.pending.tx++; + this.put(layout.R, this.pending.commit()); + + this.unlockTX(tx); + + this.emit('tx', tx, details); + + if (tx.height !== -1) + this.emit('confirmed', tx, details); + + this.emit('balance', this.pending.toBalance(), details); + + return details; +}); + /** * Attempt to confirm a transaction. * @private @@ -996,102 +937,77 @@ TXDB.prototype.isSpent = co(function* isSpent(hash, index) { * transaction was confirmed, or should be ignored. */ -TXDB.prototype.confirm = co(function* confirm(tx, existing) { +TXDB.prototype.confirm = co(function* confirm(tx) { var hash = tx.hash('hex'); - var i, account, output, coin; - var input, prevout, path, spender, coins; - var key, raw, credit, details; + var details = new Details(this, tx); + var i, account, output, coin, input, prevout; + var path, credit, credits; - // Inject block properties. - existing.ts = tx.ts; - existing.height = tx.height; - existing.index = tx.index; - existing.block = tx.block; - tx = existing; - - details = new Details(this, tx); - - this.put(layout.t(hash), tx.toExtended()); - - this.del(layout.p(hash)); - this.put(layout.h(tx.height, hash), DUMMY); - - // Consume unspent money or add orphans if (!tx.isCoinbase()) { - coins = yield this.fillHistory(tx); + credits = yield this.getSpentCredits(tx); for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; prevout = input.prevout; - coin = coins[i]; + credit = credits[i]; - if (!coin) { - coin = yield this.getCoin(prevout.hash, prevout.index); + // There may be new credits available + // that we haven't seen yet. + if (!credit) { + credit = yield this.getCredit(prevout.hash, prevout.index); - if (!coin) + if (!credit) continue; - spender = Outpoint.fromTX(tx, i).toRaw(); + this.spendCredit(credit, tx, i); - this.put(layout.d(hash, i), coin.toRaw()); - this.put(layout.s(prevout.hash, prevout.index), spender); - - this.pending.unconfirmed -= coin.value; + this.pending.unconfirmed -= credit.coin.value; } + coin = credit.coin; + assert(coin.height !== -1); - input.coin = coin; - - // Only bother if this input is ours. path = yield this.getPath(coin); assert(path); - details.addInput(i, path, coin); - - key = prevout.hash + prevout.index; - - this.del(layout.c(prevout.hash, prevout.index)); - this.del(layout.C(path.account, prevout.hash, prevout.index)); + details.setInput(i, path, coin); this.pending.coin--; this.pending.confirmed -= coin.value; - this.coinCache.remove(key); + this.removeCredit(credit, path); } } for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); - key = hash + i; - // Only update coins if this output is ours. if (!path) continue; - details.addOutput(i, path); - - // Update spent coin. - yield this.updateSpentCoin(tx, i); + details.setOutput(i, path); credit = yield this.getCredit(hash, i); + assert(credit); - if (!credit) - continue; + if (credit.spent) + yield this.updateSpentCoin(tx, i); coin = credit.coin; coin.height = tx.height; - raw = credit.toRaw(); - this.pending.confirmed += coin.value; this.pending.coin++; + this.pending.confirmed += output.value; - this.put(layout.c(hash, i), raw); - - this.coinCache.set(key, raw); + this.saveCredit(credit, path); } + this.put(layout.t(hash), tx.toExtended()); + this.del(layout.p(hash)); + this.put(layout.h(tx.height, hash), DUMMY); + for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; this.del(layout.P(account, hash)); @@ -1100,7 +1016,6 @@ TXDB.prototype.confirm = co(function* confirm(tx, existing) { this.put(layout.R, this.pending.commit()); - // Clear any locked coins to free up memory. this.unlockTX(tx); this.emit('tx', tx, details); @@ -1117,42 +1032,14 @@ TXDB.prototype.confirm = co(function* confirm(tx, existing) { */ TXDB.prototype.remove = co(function* remove(hash) { - var details; - - this.start(); - - try { - details = yield this._remove(hash); - } catch (e) { - this.drop(); - throw e; - } - - yield this.commit(); - - return details; -}); - -/** - * Remove a transaction without a lock. - * @private - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype._remove = co(function* remove(hash) { var tx = yield this.getTX(hash); var details; if (!tx) return; - this.drop(); - details = yield this.removeRecursive(tx); - this.start(); - if (!details) return; @@ -1160,15 +1047,41 @@ TXDB.prototype._remove = co(function* remove(hash) { }); /** - * Remove a transaction from the database, but do not - * look up the transaction. Use the passed-in transaction - * to disconnect. - * @param {TX} tx - * @returns {Promise} + * Remove a transaction and recursively + * remove all of its spenders. + * @private + * @param {TX} tx - Transaction to be removed. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.lazyRemove = co(function* lazyRemove(tx) { - return yield this.__remove(tx); +TXDB.prototype.removeRecursive = co(function* removeRecursive(tx) { + var hash = tx.hash('hex'); + var i, spent, stx, details; + + for (i = 0; i < tx.outputs.length; i++) { + spent = yield this.getSpent(hash, i); + + if (!spent) + continue; + + // Remove all of the spender's spenders first. + stx = yield this.getTX(spent.hash); + + assert(stx); + + yield this.removeRecursive(stx); + } + + this.start(); + + // Remove the spender. + details = yield this._remove(tx); + + assert(details); + + yield this.commit(); + + return details; }); /** @@ -1178,12 +1091,59 @@ TXDB.prototype.lazyRemove = co(function* lazyRemove(tx) { * @returns {Promise} */ -TXDB.prototype.__remove = co(function* remove(tx) { +TXDB.prototype._remove = co(function* remove(tx) { var hash = tx.hash('hex'); - var i, path, account, key, prevout; - var input, output, coin, coins, raw, credit, details; + var details = new Details(this, tx); + var i, path, account, credits; + var input, output, coin, credit; - details = new Details(this, tx); + if (!tx.isCoinbase()) { + credits = yield this.getSpentCredits(tx); + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + credit = credits[i]; + + if (!credit) + continue; + + coin = credit.coin; + path = yield this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + this.pending.unconfirmed += coin.value; + + if (tx.height !== -1) { + this.pending.coin++; + this.pending.confirmed += coin.value; + } + + this.unspendCredit(tx, i); + this.saveCredit(credit, path); + } + } + + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + path = yield this.getPath(output); + + if (!path) + continue; + + details.setOutput(i, path); + + credit = Credit.fromTX(tx, i); + + this.pending.coin--; + this.pending.unconfirmed -= output.value; + + if (tx.height !== -1) + this.pending.confirmed -= output.value; + + this.removeCredit(credit, path); + } this.del(layout.t(hash)); @@ -1194,69 +1154,6 @@ TXDB.prototype.__remove = co(function* remove(tx) { this.del(layout.m(tx.ps, hash)); - if (!tx.isCoinbase()) { - coins = yield this.fillHistory(tx); - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - key = input.prevout.hash + input.prevout.index; - prevout = input.prevout; - coin = coins[i]; - - if (!coin) - continue; - - path = yield this.getPath(coin); - assert(path); - - credit = new Credit(coin); - - details.addInput(i, path, coin); - - this.pending.unconfirmed += coin.value; - - this.del(layout.s(prevout.hash, prevout.index)); - - if (tx.height !== -1) { - raw = credit.toRaw(); - this.pending.confirmed += coin.value; - this.pending.coin++; - this.put(layout.c(prevout.hash, prevout.index), raw); - this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); - this.coinCache.set(key, raw); - } else { - credit.spent = false; - raw = credit.toRaw(); - this.put(layout.c(prevout.hash, prevout.index), raw); - this.coinCache.set(key, raw); - } - - this.del(layout.d(hash, i)); - } - } - - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - key = hash + i; - path = yield this.getPath(output); - - if (!path) - continue; - - details.addOutput(i, path); - - this.pending.coin--; - this.pending.unconfirmed -= output.value; - - if (tx.height !== -1) - this.pending.confirmed -= output.value; - - this.del(layout.c(hash, i)); - this.del(layout.C(path.account, hash, i)); - - this.coinCache.remove(key); - } - for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; @@ -1311,14 +1208,11 @@ TXDB.prototype.unconfirm = co(function* unconfirm(hash) { TXDB.prototype._unconfirm = co(function* unconfirm(hash) { var tx = yield this.getTX(hash); - var details; if (!tx) return false; - details = yield this.__unconfirm(tx); - - return details; + return yield this.__unconfirm(tx); }); /** @@ -1329,87 +1223,75 @@ TXDB.prototype._unconfirm = co(function* unconfirm(hash) { TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { var hash = tx.hash('hex'); + var details = new Details(this, tx); var height = tx.height; - var i, account, output, key, coin, coins; - var input, prevout, path, spender, raw, credit; + var i, account, output, coin, credits; + var input, path, credit; - if (height === -1) - return; + assert(height !== -1); tx.unsetBlock(); - details = new Details(this, tx); - - this.put(layout.t(hash), tx.toExtended()); - - this.put(layout.p(hash), DUMMY); - this.del(layout.h(height, hash)); - - // Consume unspent money or add orphans if (!tx.isCoinbase()) { - coins = yield this.fillHistory(tx); + credits = yield this.getSpentCredits(tx); for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; - prevout = input.prevout; - coin = coins[i]; + credit = credits[i]; - // Only bother if this input is ours. - if (!coin) + if (!credit) continue; - assert(coin.height !== -1); + coin = credit.coin; - credit = new Credit(coin); + assert(coin.height !== -1); path = yield this.getPath(coin); assert(path); - details.addInput(i, path, coin); - - key = prevout.hash + prevout.index; - - spender = Outpoint.fromTX(tx, i).toRaw(); - - credit.spent = true; - raw = credit.toRaw(); - this.put(layout.c(prevout.hash, prevout.index), raw); - this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); + details.setInput(i, path, coin); this.pending.coin++; this.pending.confirmed += coin.value; - this.coinCache.set(key, raw); + credit.spent = true; + + this.saveCredit(credit, path); } } for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); - key = hash + i; - // Update spent coin. - yield this.updateSpentCoin(tx, i); + if (!path) + continue; credit = yield this.getCredit(hash, i); - if (!credit) + if (!credit) { + yield this.updateSpentCoin(tx, i); continue; + } - details.addOutput(i, path); + if (credit.spent) + yield this.updateSpentCoin(tx, i); + + details.setOutput(i, path); coin = credit.coin; coin.height = -1; - raw = credit.toRaw(); - this.pending.confirmed -= coin.value; this.pending.coin++; + this.pending.confirmed -= output.value; - this.put(layout.c(hash, i), raw); - - this.coinCache.set(key, raw); + this.saveCredit(credit, path); } + this.put(layout.t(hash), tx.toExtended()); + this.put(layout.p(hash), DUMMY); + this.del(layout.h(height, hash)); + for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; this.put(layout.P(account, hash), DUMMY); @@ -1424,6 +1306,74 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { return details; }); +/** + * Remove spenders that have not been confirmed. We do this in the + * odd case of stuck transactions or when a coin is double-spent + * by a newer transaction. All previously-spending transactions + * of that coin that are _not_ confirmed will be removed from + * the database. + * @private + * @param {Hash} hash + * @param {TX} ref - Reference tx, the tx that double-spent. + * @returns {Promise} - Returns Boolean. + */ + +TXDB.prototype.removeConflict = co(function* removeConflict(hash, ref) { + var tx = yield this.getTX(hash); + var details; + + assert(tx); + + this.logger.warning('Handling conflicting tx: %s.', utils.revHex(hash)); + + this.drop(); + + details = yield this.removeRecursive(tx); + + this.start(); + + this.logger.warning('Removed conflict: %s.', tx.rhash); + + // Emit the _removed_ transaction. + this.emit('conflict', tx, details); + + return details; +}); + +/** + * Retrieve coins for own inputs, remove + * double spenders, and verify inputs. + * @private + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype.removeConflicts = co(function* removeConflicts(tx) { + var hash = tx.hash('hex'); + var i, input, prevout, spent; + + if (tx.isCoinbase()) + return; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; + + // Is it already spent? + spent = yield this.getSpent(prevout.hash, prevout.index); + + if (!spent) + continue; + + // Did _we_ spend it? + if (spent.hash === hash) + continue; + + // Remove the double spender. + yield this.removeConflict(spent.hash, tx); + } +}); + /** * Lock all coins in a transaction. * @param {TX} tx @@ -1850,9 +1800,8 @@ TXDB.prototype.getHistory = function getHistory(account) { TXDB.prototype.getAccountHistory = co(function* getAccountHistory(account) { var txs = []; - var i, hashes, hash, tx; - - hashes = yield this.getHistoryHashes(account); + var hashes = yield this.getHistoryHashes(account); + var i, hash, tx; for (i = 0; i < hashes.length; i++) { hash = hashes[i]; @@ -1875,9 +1824,8 @@ TXDB.prototype.getAccountHistory = co(function* getAccountHistory(account) { TXDB.prototype.getPending = co(function* getPending(account) { var txs = []; - var i, hashes, hash, tx; - - hashes = yield this.getPendingHashes(account); + var hashes = yield this.getPendingHashes(account); + var i, hash, tx; for (i = 0; i < hashes.length; i++) { hash = hashes[i]; @@ -2037,6 +1985,38 @@ TXDB.prototype.fillHistory = co(function* fillHistory(tx) { return coins; }); +/** + * Fill a transaction with coins (all historical coins). + * @param {TX} tx + * @returns {Promise} - Returns {@link TX}. + */ + +TXDB.prototype.getSpentCredits = co(function* getSpentCredits(tx) { + var credits = []; + var hash; + + if (tx.isCoinbase()) + return credits; + + hash = tx.hash('hex'); + + yield this.range({ + gte: layout.d(hash, 0x00000000), + lte: layout.d(hash, 0xffffffff), + parse: function(key, value) { + var index = layout.dd(key)[1]; + var coin = Coin.fromRaw(value); + var input = tx.inputs[index]; + assert(input); + coin.hash = input.prevout.hash; + coin.index = input.prevout.index; + credits[index] = new Credit(coin); + } + }); + + return credits; +}); + /** * Fill a transaction with coins. * @param {TX} tx @@ -2064,7 +2044,7 @@ TXDB.prototype.fillCoins = co(function* fillCoins(tx) { if (credit.spent) continue; - input.coin = coin; + input.coin = credit.coin; } return tx; @@ -2122,7 +2102,7 @@ TXDB.prototype.getDetails = co(function* getDetails(hash) { TXDB.prototype.toDetails = co(function* toDetails(tx) { var i, out, txs, details; - var coins, coin, path, input, output; + var coins, coin, path, output; if (Array.isArray(tx)) { out = []; @@ -2148,13 +2128,13 @@ TXDB.prototype.toDetails = co(function* toDetails(tx) { for (i = 0; i < tx.inputs.length; i++) { coin = coins[i]; path = yield this.getPath(coin); - details.addInput(i, path, coin); + details.setInput(i, path, coin); } for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); - details.addOutput(i, path); + details.setOutput(i, path); } return details; @@ -2577,7 +2557,7 @@ Details.prototype.init = function init() { } }; -Details.prototype.addInput = function addInput(i, path, coin) { +Details.prototype.setInput = function setInput(i, path, coin) { var member = this.inputs[i]; if (coin) { @@ -2591,7 +2571,7 @@ Details.prototype.addInput = function addInput(i, path, coin) { } }; -Details.prototype.addOutput = function addOutput(i, path) { +Details.prototype.setOutput = function setOutput(i, path) { var member = this.outputs[i]; if (path) { @@ -2663,9 +2643,15 @@ DetailsMember.prototype.toJSON = function toJSON(network) { }; }; +/** + * Credit + * @constructor + */ + function Credit(coin, spent) { if (!(this instanceof Credit)) return new Credit(coin, spent); + this.coin = coin || new Coin(); this.spent = spent || false; } diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 8a35f26b..c39dca34 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -26,7 +26,6 @@ var HD = require('../hd/hd'); var Account = require('./account'); var MasterKey = require('./masterkey'); var LRU = require('../utils/lru'); -var PathInfo = require('./pathinfo'); /** * BIP44 Wallet diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index be8a8001..ea9d28c1 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1234,9 +1234,13 @@ WalletDB.prototype.getWalletsByHashes = co(function* getWalletsByHashes(tx) { */ WalletDB.prototype.getWalletsByInsert = co(function* getWalletsByInsert(tx) { - var result = []; - var hashes = tx.getOutputHashes('hex'); - var i, j, input, hash, wids; + var i, j, result, hashes, input, hash, wids; + + if (this.options.resolution) + return yield this.getWalletsByHashes(tx); + + result = []; + hashes = tx.getOutputHashes('hex'); for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; diff --git a/test/chain-test.js b/test/chain-test.js index ab8143d2..a6f77ec1 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -19,6 +19,7 @@ describe('Chain', function() { node = new bcoin.fullnode({ db: 'memory' }); chain = node.chain; walletdb = node.walletdb; + walletdb.options.resolution = false; miner = node.miner; node.on('error', function() {});