txdb: comments. serialization.

This commit is contained in:
Christopher Jeffrey 2016-10-19 14:20:57 -07:00
parent 557c1044cc
commit 2f72937686
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
2 changed files with 407 additions and 130 deletions

View File

@ -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

View File

@ -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);
});