diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index 06ed05d4..db11abaf 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -2503,6 +2503,133 @@ TX.fromExtended = function fromExtended(data, saveCoins, enc) { return new TX().fromExtended(data, saveCoins); }; +/** + * Serialize a transaction to BCoin "extended format". + * This is the serialization format BCoin uses internally + * to store transactions in the database. The extended + * serialization includes the height, block hash, index, + * timestamp, pending-since time, and optionally a vector + * for the serialized coins. + * @param {Boolean?} saveCoins - Whether to serialize the coins. + * @returns {Buffer} + */ + +TX.prototype.toExtended2 = function toExtended2(saveCoins, writer) { + var p = BufferWriter(writer); + var i, input, field, bit, oct; + + this.toRaw(p); + + p.writeU32(this.ps); + + if (this.height !== -1) { + assert(this.block != null); + assert(this.height !== -1); + assert(this.index !== -1); + assert(this.ts !== 0); + p.writeU8(1); + p.writeHash(this.block); + p.writeU32(this.height); + p.writeU32(this.ts); + p.writeU32(this.index); + } else { + p.writeU8(0); + } + + if (saveCoins) { + field = new Buffer(Math.ceil(this.inputs.length / 8)); + field.fill(0); + + p.writeBytes(field); + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + + if (!input.coin) { + bit = i % 8; + oct = (i - bit) / 8; + field[oct] |= 1 << (7 - bit); + continue; + } + + input.coin.toRaw(p); + } + } + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from "extended" serialization format. + * @param {Buffer} data + * @param {Boolean?} saveCoins - If true, the function will + * attempt to parse the coins. + */ + +TX.prototype.fromExtended2 = function fromExtended2(data, saveCoins) { + var p = BufferReader(data); + var i, input, coin, field, bit, oct, spent; + + this.fromRaw(p); + + this.ps = p.readU32(); + + if (p.readU8() == 1) { + this.block = p.readHash('hex'); + this.height = p.readU32(); + this.ts = p.readU32(); + this.index = p.readU32(); + } + + if (saveCoins) { + field = p.readBytes(Math.ceil(this.inputs.length / 8), true); + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + + bit = i % 8; + oct = (i - bit) / 8; + spent = (field[oct] >>> (7 - bit)) & 1; + + if (spent) + continue; + + coin = Coin.fromRaw(p); + coin.hash = input.prevout.hash; + coin.index = input.prevout.index; + + input.coin = coin; + } + } + + return this; +}; + +/** + * Instantiate a transaction from a Buffer + * in "extended" serialization format. + * @param {Buffer} data + * @param {Boolean?} saveCoins - If true, the function will + * attempt to parse the coins. + * @param {String?} enc - One of `"hex"` or `null`. + * @returns {TX} + */ + +TX.fromExtended2 = function fromExtended2(data, saveCoins, enc) { + if (typeof saveCoins === 'string') { + enc = saveCoins; + saveCoins = false; + } + + if (typeof data === 'string') + data = new Buffer(data, enc); + + return new TX().fromExtended2(data, saveCoins); +}; + /** * Test whether an object is a TX. * @param {Object} obj diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 629fabb8..4409b2e8 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -491,6 +491,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { if (tx.isCoinbase()) return true; + // We already have this one. if (this.count[hash]) return false; @@ -498,6 +499,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { input = tx.inputs[i]; prevout = input.prevout; + // If we have a coin, we can verify this right now. coin = yield this.getCoin(prevout.hash, prevout.index); if (coin) { @@ -510,27 +512,37 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { continue; } + // Check whether the input is already part of the stxo set. spent = yield this.getSpent(prevout.hash, prevout.index); if (spent) { coin = yield this.getSpentCoin(spent, prevout); - if (!coin) + // If we don't have an undo coin, + // this is an unknown input we + // decided to index for the hell + // of it. + if (coin) { + if (this.options.verify && tx.height === -1) { + input.coin = coin; + if (!(yield tx.verifyInputAsync(i))) + return false; + } continue; - - if (this.options.verify && tx.height === -1) { - input.coin = coin; - if (!(yield tx.verifyInputAsync(i))) - return false; } - - continue; } + // This is a HACK: try to extract an + // address from the input to tell if + // it's ours and should be regarded + // as an orphan input. if (yield this.hasPath(input)) orphans[i] = true; } + // Store orphans now that we've + // fully verified everything to + // the best of our ability. for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; prevout = input.prevout; @@ -540,6 +552,11 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { key = prevout.hash + prevout.index; + // In theory, someone could try to DoS + // us by creating tons of fake transactions + // with our pubkey in the scriptsig. This + // is due to the hack mentioned above. Only + //allow 20 orphans at a time. if (this.totalOrphans > 20) { this.logger.warning('Potential orphan flood!'); this.logger.warning( @@ -563,6 +580,8 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { hasOrphans = true; } + // We wait til _all_ orphans are resolved + // before inserting this transaction. if (hasOrphans) return false; @@ -584,6 +603,10 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { if (!resolved) resolved = []; + // Always push the first transaction on. + // Necessary for the first resolved tx + // as well as the recursive behavior + // below. resolved.push(tx); for (i = 0; i < tx.outputs.length; i++) { @@ -598,6 +621,12 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { coin = Coin.fromTX(tx, i); + // Note that their might be multiple + // orphans per input, either due to + // double spends or an adversary + // creating fake transactions. We + // take the first valid orphan + // transaction. for (j = 0; j < orphans.length; j++) { orphan = orphans[j]; valid = true; @@ -607,11 +636,14 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { assert(input.prevout.hash === hash); assert(input.prevout.index === i); + // We can finally verify this input. if (this.options.verify && orphan.tx.height === -1) { input.coin = coin; valid = yield orphan.tx.verifyInputAsync(orphan.index); } + // If it's valid and fully resolved, + // we can resolve _its_ outputs. if (valid) { if (--this.count[orphan.hash] === 0) { delete this.count[orphan.hash]; @@ -620,6 +652,7 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { break; } + // Forget about it if invalid. delete this.count[orphan.hash]; } } @@ -722,16 +755,27 @@ TXDB.prototype.resolveInput = co(function* resolveInput(tx, i, path) { if (!spent) return false; + // If we have an undo coin, we + // already knew about this input. if (yield this.hasSpentCoin(spent)) return false; + // Get the spending transaction so + // we can properly add the undo coin. stx = yield this.getTX(spent.hash); assert(stx); + // Crete the credit and add the undo coin. credit = Credit.fromTX(tx, i); this.spendCredit(credit, stx, spent.index); + // If the spender is unconfirmed, save + // the credit as well, and mark it as + // unspent in the mempool. This is the + // same behavior `insert` would have + // done for inputs. We're just doing + // it retroactively. if (stx.height === -1) { credit.spent = true; this.saveCredit(credit, path); @@ -816,7 +860,8 @@ TXDB.prototype.isSpent = co(function* isSpent(hash, index) { }); /** - * Add transaction, runs `confirm()` and `verify()`. + * Add transaction, potentially runs + * `confirm()` and `removeConflicts()`. * @param {TX} tx * @returns {Promise} */ @@ -839,7 +884,7 @@ TXDB.prototype.add = co(function* add(tx) { }); /** - * Add transaction without a lock. + * Add transaction without a batch. * @private * @param {TX} tx * @returns {Promise} @@ -864,7 +909,7 @@ TXDB.prototype._add = co(function* add(tx) { // Save the original time. tx.ps = existing.ps; - // Attempt to confirm tx before adding it. + // Confirm transaction. return yield this.confirm(tx); } @@ -876,6 +921,9 @@ TXDB.prototype._add = co(function* add(tx) { // We ignore any unconfirmed txs // that are replace-by-fee. if (yield this.isRBF(tx)) { + // We need to index every spender + // hash to detect "passive" + // replace-by-fee. this.put(layout.r(hash), DUMMY); return; } @@ -887,29 +935,42 @@ TXDB.prototype._add = co(function* add(tx) { this.del(layout.r(hash)); } - return yield this._insert(tx); + // Finally we can do a regular insertion. + return yield this.insert(tx); }); /** - * Add transaction without a lock. + * Insert transaction. * @private * @param {TX} tx * @returns {Promise} */ -TXDB.prototype._insert = co(function* insert(tx) { +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()) { + // We need to potentially spend some coins here. 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) { + // Maintain an stxo list for every + // spent input (even ones we don't + // recognize). This is used for + // detecting double-spends (as best + // we can), as well as resolving + // inputs we didn't know were ours + // at the time. This built-in error + // correction is not technically + // necessary assuming no messages + // are ever missed from the mempool, + // but shit happens. this.writeInput(tx, i); continue; } @@ -918,16 +979,37 @@ TXDB.prototype._insert = co(function* insert(tx) { path = yield this.getPath(coin); assert(path); + // Build the tx details object + // as we go, for speed. details.setInput(i, path, coin); + // Write an undo coin for the credit + // and add it to the stxo set. this.spendCredit(credit, tx, i); + // Unconfirmed balance should always + // be updated as it reflects the on-chain + // balance _and_ mempool balance assuming + // everything in the mempool were to confirm. this.pending.unconfirmed -= coin.value; if (tx.height === -1) { + // If the tx is not mined, we do not + // disconnect the coin, we simply mark + // a `spent` flag on the credit. This + // effectively prevents the mempool + // from altering our utxo state + // permanently. It also makes it + // possible to compare the on-chain + // state vs. the mempool state. credit.spent = true; this.saveCredit(credit, path); } else { + // If the tx is mined, we can safely + // remove the coin being spent. This + // coin will be indexed as an undo + // coin so it can be reconnected + // later during a reorg. this.pending.coin--; this.pending.confirmed -= coin.value; this.removeCredit(credit, path); @@ -935,6 +1017,7 @@ TXDB.prototype._insert = co(function* insert(tx) { } } + // Potentially add coins to the utxo set. for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); @@ -944,6 +1027,8 @@ TXDB.prototype._insert = co(function* insert(tx) { details.setOutput(i, path); + // Attempt to resolve an input we + // did not know was ours at the time. if (yield this.resolveInput(tx, i, path)) continue; @@ -958,6 +1043,8 @@ TXDB.prototype._insert = co(function* insert(tx) { this.saveCredit(credit, path); } + // Save and index the transaction in bcoin's + // own "extended" transaction serialization. this.put(layout.t(hash), tx.toExtended()); this.put(layout.m(tx.ps, hash), DUMMY); @@ -966,6 +1053,9 @@ TXDB.prototype._insert = co(function* insert(tx) { else this.put(layout.h(tx.height, hash), DUMMY); + // Do some secondary indexing for account-based + // queries. This saves us a lot of time for + // queries later. for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; @@ -978,16 +1068,23 @@ TXDB.prototype._insert = co(function* insert(tx) { this.put(layout.H(account, tx.height, hash), DUMMY); } + // Update the transaction counter and + // commit the new state. This state will + // only overwrite the best state once + // the batch has actually been written + // to disk. this.pending.tx++; this.put(layout.R, this.pending.commit()); + // This transaction may unlock some + // coins now that we've seen it. this.unlockTX(tx); + // Emit events for potential local and + // websocket listeners. Note that these + // will only be emitted if the batch is + // successfully written to disk. this.emit('tx', tx, details); - - if (tx.height !== -1) - this.emit('confirmed', tx, details); - this.emit('balance', this.pending.toBalance(), details); return details; @@ -997,9 +1094,7 @@ TXDB.prototype._insert = co(function* insert(tx) { * Attempt to confirm a transaction. * @private * @param {TX} tx - * @returns {Promise} - Returns Boolean. `false` if - * the transaction should be added to the database, `true` if the - * transaction was confirmed, or should be ignored. + * @returns {Promise} */ TXDB.prototype.confirm = co(function* confirm(tx) { @@ -1011,6 +1106,9 @@ TXDB.prototype.confirm = co(function* confirm(tx) { if (!tx.isCoinbase()) { credits = yield this.getSpentCredits(tx); + // Potentially spend coins. Now that the tx + // is mined, we can actually _remove_ coins + // from the utxo state. for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; prevout = input.prevout; @@ -1024,6 +1122,11 @@ TXDB.prototype.confirm = co(function* confirm(tx) { if (!credit) continue; + // Add a spend record and undo coin + // for the coin we now know is ours. + // We don't need to remove the coin + // since it was never added in the + // first place. this.spendCredit(credit, tx, i); this.pending.unconfirmed -= credit.coin.value; @@ -1038,6 +1141,9 @@ TXDB.prototype.confirm = co(function* confirm(tx) { details.setInput(i, path, coin); + // We can now safely remove the credit + // entirely, now that we know it's also + // been removed on-chain. this.pending.coin--; this.pending.confirmed -= coin.value; @@ -1045,6 +1151,7 @@ TXDB.prototype.confirm = co(function* confirm(tx) { } } + // Update credit heights, including undo coins. for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); @@ -1054,15 +1161,18 @@ TXDB.prototype.confirm = co(function* confirm(tx) { details.setOutput(i, path); - if (yield this.resolveInput(tx, i, path)) - continue; - credit = yield this.getCredit(hash, i); assert(credit); + // Credits spent in the mempool add an + // undo coin for ease. If this credit is + // spent in the mempool, we need to + // update the undo coin's height. if (credit.spent) yield this.updateSpentCoin(tx, i); + // Update coin height and confirmed + // balance. Save once again. coin = credit.coin; coin.height = tx.height; @@ -1072,21 +1182,25 @@ TXDB.prototype.confirm = co(function* confirm(tx) { this.saveCredit(credit, path); } + // Save the new serialized transaction as + // the block-related properties have been + // updated. Also reindex for height. this.put(layout.t(hash), tx.toExtended()); this.del(layout.p(hash)); this.put(layout.h(tx.height, hash), DUMMY); + // Secondary indexing also needs to change. for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; this.del(layout.P(account, hash)); this.put(layout.H(account, tx.height, hash), DUMMY); } + // Commit the new state. The balance has updated. this.put(layout.R, this.pending.commit()); this.unlockTX(tx); - this.emit('tx', tx, details); this.emit('confirmed', tx, details); this.emit('balance', this.pending.toBalance(), details); @@ -1094,22 +1208,127 @@ TXDB.prototype.confirm = co(function* confirm(tx) { }); /** - * Remove a transaction from the database. Disconnect inputs. + * Recursively remove a transaction + * from the database. * @param {Hash} hash * @returns {Promise} */ TXDB.prototype.remove = co(function* remove(hash) { var tx = yield this.getTX(hash); - var details; if (!tx) return; - details = yield this.removeRecursive(tx); + return yield this.removeRecursive(tx); +}); - if (!details) - return; +/** + * Remove a transaction from the + * database. Disconnect inputs. + * @private + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype.erase = co(function* erase(tx) { + var hash = tx.hash('hex'); + var details = new Details(this, tx); + var i, path, account, credits; + var input, output, coin, credit; + + if (!tx.isCoinbase()) { + // We need to undo every part of the + // state this transaction ever touched. + // Start by getting the undo coins. + credits = yield this.getSpentCredits(tx); + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + credit = credits[i]; + + if (!credit) { + // This input never had an undo + // coin, but remove it from the + // stxo set. + this.removeInput(tx, i); + continue; + } + + coin = credit.coin; + path = yield this.getPath(coin); + assert(path); + + details.setInput(i, path, coin); + + // Recalculate the balance, remove + // from stxo set, remove the undo + // coin, and resave the credit. + 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); + } + } + + // We need to remove all credits + // this transaction created. + 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); + } + + // Remove the transaction data + // itself as well as unindex. + this.del(layout.t(hash)); + this.del(layout.m(tx.ps, hash)); + + if (tx.height === -1) + this.del(layout.p(hash)); + else + this.del(layout.h(tx.height, hash)); + + // Remove all secondary indexing. + for (i = 0; i < details.accounts.length; i++) { + account = details.accounts[i]; + + this.del(layout.T(account, hash)); + this.del(layout.M(account, tx.ps, hash)); + + if (tx.height === -1) + this.del(layout.P(account, hash)); + else + this.del(layout.H(account, tx.height, hash)); + } + + // Update the transaction counter + // and commit new state due to + // balance change. + this.pending.tx--; + this.put(layout.R, this.pending.commit()); + + this.emit('remove tx', tx, details); + this.emit('balance', this.pending.toBalance(), details); return details; }); @@ -1119,7 +1338,7 @@ TXDB.prototype.remove = co(function* remove(hash) { * remove all of its spenders. * @private * @param {TX} tx - Transaction to be removed. - * @returns {Promise} - Returns Boolean. + * @returns {Promise} */ TXDB.prototype.removeRecursive = co(function* removeRecursive(tx) { @@ -1143,7 +1362,7 @@ TXDB.prototype.removeRecursive = co(function* removeRecursive(tx) { this.start(); // Remove the spender. - details = yield this._remove(tx); + details = yield this.erase(tx); assert(details); @@ -1153,99 +1372,7 @@ TXDB.prototype.removeRecursive = co(function* removeRecursive(tx) { }); /** - * Remove a transaction from the database. Disconnect inputs. - * @private - * @param {TX} tx - * @returns {Promise} - */ - -TXDB.prototype._remove = co(function* remove(tx) { - var hash = tx.hash('hex'); - var details = new Details(this, tx); - var i, path, account, credits; - var input, output, coin, credit; - - 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) { - this.removeInput(tx, i); - 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)); - this.del(layout.m(tx.ps, hash)); - - if (tx.height === -1) - this.del(layout.p(hash)); - else - this.del(layout.h(tx.height, hash)); - - for (i = 0; i < details.accounts.length; i++) { - account = details.accounts[i]; - - this.del(layout.T(account, hash)); - this.del(layout.M(account, tx.ps, hash)); - - if (tx.height === -1) - this.del(layout.P(account, hash)); - else - this.del(layout.H(account, tx.height, hash)); - } - - this.pending.tx--; - this.put(layout.R, this.pending.commit()); - - this.emit('remove tx', tx, details); - this.emit('balance', this.pending.toBalance(), details); - - return details; -}); - -/** - * Unconfirm a transaction. This is usually necessary after a reorg. + * Unconfirm a transaction. Necessary after a reorg. * @param {Hash} hash * @returns {Promise} */ @@ -1268,7 +1395,7 @@ TXDB.prototype.unconfirm = co(function* unconfirm(hash) { }); /** - * Unconfirm a transaction without a lock. + * Unconfirm a transaction without a batch. * @private * @param {Hash} hash * @returns {Promise} @@ -1280,16 +1407,16 @@ TXDB.prototype._unconfirm = co(function* unconfirm(hash) { if (!tx) return; - return yield this.__unconfirm(tx); + return yield this.disconnect(tx); }); /** - * Unconfirm a transaction. This is usually necessary after a reorg. + * Unconfirm a transaction. Necessary after a reorg. * @param {Hash} hash * @returns {Promise} */ -TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { +TXDB.prototype.disconnect = co(function* disconnect(tx) { var hash = tx.hash('hex'); var details = new Details(this, tx); var height = tx.height; @@ -1301,6 +1428,9 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { tx.unsetBlock(); if (!tx.isCoinbase()) { + // We need to reconnect the coins. Start + // by getting all of the undo coins we know + // about. credits = yield this.getSpentCredits(tx); for (i = 0; i < tx.inputs.length; i++) { @@ -1322,12 +1452,15 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { this.pending.coin++; this.pending.confirmed += coin.value; + // Resave the credit and mark it + // as spent in the mempool instead. credit.spent = true; - this.saveCredit(credit, path); } } + // We need to remove heights on + // the credits and undo coins. for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; path = yield this.getPath(output); @@ -1337,6 +1470,13 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { credit = yield this.getCredit(hash, i); + assert(credit); + assert(!credit.spent); + + // Neither of the spent cases below + // should ever happen, but account + // for them if some jackass is messing + // around with bcoin. if (!credit) { yield this.updateSpentCoin(tx, i); continue; @@ -1347,6 +1487,8 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { details.setOutput(i, path); + // Update coin height and confirmed + // balance. Save once again. coin = credit.coin; coin.height = -1; @@ -1356,16 +1498,22 @@ TXDB.prototype.__unconfirm = co(function* unconfirm(tx) { this.saveCredit(credit, path); } + // We need to update the now-removed + // block properties and reindex due + // to the height change. this.put(layout.t(hash), tx.toExtended()); this.put(layout.p(hash), DUMMY); this.del(layout.h(height, hash)); + // Secondary indexing also needs to change. for (i = 0; i < details.accounts.length; i++) { account = details.accounts[i]; this.put(layout.P(account, hash), DUMMY); this.del(layout.H(account, height, hash)); } + // Commit state due to unconfirmed + // vs. confirmed balance change. this.put(layout.R, this.pending.commit()); this.emit('unconfirmed', tx, details); @@ -2443,8 +2591,10 @@ TXDB.prototype.zap = co(function* zap(account, age) { TXDB.prototype.abandon = co(function* abandon(hash) { var result = yield this.has(layout.p(hash)); + if (!result) throw new Error('TX not eligible.'); + return yield this.remove(hash); });