txdb: fix double-spend handling.

This commit is contained in:
Christopher Jeffrey 2016-06-04 19:22:09 -07:00
parent a6ca11611f
commit af33e61031
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 219 additions and 171 deletions

View File

@ -73,38 +73,18 @@ TXDB.prototype._lock = function _lock(func, args, force) {
TXDB.prototype._loadFilter = function loadFilter(callback) { TXDB.prototype._loadFilter = function loadFilter(callback) {
var self = this; var self = this;
var iter;
if (!this.filter) if (!this.filter)
return callback(); return callback();
iter = this.db.iterator({ this.db.iterate({
gte: 'W', gte: 'W',
lte: 'W~', lte: 'W~',
keys: true, transform: function(key) {
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);
key = key.split('/')[1]; key = key.split('/')[1];
self.filter.add(key, 'hex'); self.filter.add(key, 'hex');
}
next(); }, callback);
});
})();
}; };
TXDB.prototype._testFilter = function _testFilter(addresses) { 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) { TXDB.prototype.add = function add(tx, callback, force) {
var self = this; 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) { return this.getMap(tx, function(err, map) {
if (err) if (err)
return callback(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) { TXDB.prototype._add = function add(tx, map, callback, force) {
var self = this; var self = this;
var hash = tx.hash('hex');
var updated = false; 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) if (!unlock)
return; return;
@ -341,14 +310,15 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Ignore if we already have this tx. // Ignore if we already have this tx.
if (existing) if (existing)
return callback(null, true); return callback(null, true, map);
hash = tx.hash('hex');
batch = self.db.batch(); batch = self.db.batch();
batch.put('t/' + hash, tx.toExtended()); batch.put('t/' + hash, tx.toExtended());
if (tx.ts === 0) { if (tx.ts === 0) {
assert(tx.ps > 0);
batch.put('p/' + hash, DUMMY); batch.put('p/' + hash, DUMMY);
batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
} else { } else {
@ -356,8 +326,9 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
} }
map.all.forEach(function(path) { for (i = 0; i < map.all.length; i++) {
var id = path.id + '/' + path.account; path = map.all[i];
id = path.id + '/' + path.account;
batch.put('T/' + id + '/' + hash, DUMMY); batch.put('T/' + id + '/' + hash, DUMMY);
if (tx.ts === 0) { if (tx.ts === 0) {
batch.put('P/' + id + '/' + hash, DUMMY); 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('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY);
} }
}); }
// Consume unspent money or add orphans // Consume unspent money or add orphans
utils.forEachSerial(tx.inputs, function(input, next, i) { utils.forEachSerial(tx.inputs, function(input, next, i) {
var key, address;
var prevout = input.prevout; var prevout = input.prevout;
var key, address;
if (tx.isCoinbase()) if (tx.isCoinbase())
return next(); return next();
@ -388,6 +359,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
key = prevout.hash + '/' + prevout.index; key = prevout.hash + '/' + prevout.index;
batch.put('s/' + key, tx.hash());
if (coin) { if (coin) {
// Add TX to inputs and spend money // Add TX to inputs and spend money
input.coin = coin; input.coin = coin;
@ -400,28 +373,28 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
updated = true; updated = true;
if (address) { paths = map.table[address];
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account; for (j = 0; j < paths.length; j++) {
batch.del('C/' + id + '/' + key); path = paths[j];
}); id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
} }
batch.del('c/' + key); batch.del('c/' + key);
batch.put('s/' + key, tx.hash());
return next(); return next();
} }
input.coin = null; input.coin = null;
self.isSpent(prevout.hash, prevout.index, function(err, spentBy) { self.isSpent(prevout.hash, prevout.index, function(err, spent) {
if (err) if (err)
return next(err); return next(err);
// Are we double-spending? // Are we double-spending?
// Replace older txs with newer ones. // Replace older txs with newer ones.
if (spentBy) { if (spent) {
return self.getTX(prevout.hash, function(err, prev) { return self.getTX(prevout.hash, function(err, prev) {
if (err) if (err)
return next(err); return next(err);
@ -434,19 +407,22 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Skip invalid transactions // Skip invalid transactions
if (self.options.verify) { if (self.options.verify) {
if (!tx.verify(i)) 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) if (err)
return next(err); return next(err);
if (!result) { // Spender was not removed, the current
assert(tx.ts === 0, 'I\'m confused'); // transaction is not elligible to be added.
return callback(null, false); if (!rtx)
} return callback(null, false, map);
self.emit('conflict', rtx, rmap);
batch.clear(); batch.clear();
self._add(tx, map, callback, true); 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 // Add unspent outputs or resolve orphans
utils.forEachSerial(tx.outputs, function(output, next, i) { utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash(); var address = output.getHash();
var key, coin; var key = hash + '/' + i;
var coin;
// Do not add unspents for outputs that aren't ours. // Do not add unspents for outputs that aren't ours.
if (!address || !map.table[address].length) if (!address || !map.table[address].length)
return next(); return next();
key = hash + '/' + i;
if (output.script.isUnspendable()) if (output.script.isUnspendable())
return next(); return next();
@ -518,11 +493,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
return next(); return next();
} }
self.lazyRemove(orphan.tx, function(err) { self.lazyRemove(orphan.tx, next, true);
if (err)
return next(err);
return next();
}, true);
}, function(err) { }, function(err) {
if (err) if (err)
return next(err); return next(err);
@ -538,11 +509,12 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
return next(err); return next(err);
if (!orphans) { if (!orphans) {
if (address) { paths = map.table[address];
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account; for (j = 0; j < paths.length; j++) {
batch.put('C/' + id + '/' + key, DUMMY); path = paths[j];
}); id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
} }
batch.put('c/' + key, coin.toRaw()); batch.put('c/' + key, coin.toRaw());
@ -573,7 +545,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
self.emit('updated', tx, map); 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]. * @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; var self = this;
this.getTX(hash, function(err, tx) { this.getTX(hash, function(err, tx) {
if (err) if (err)
return callback(err); return callback(err);
@ -602,32 +575,77 @@ TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) {
if (!tx) if (!tx)
return callback(new Error('Could not find spender.')); return callback(new Error('Could not find spender.'));
if (tx.ts !== 0) if (tx.ts !== 0) {
return callback(null, false); // 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) // If both are confirmed but replacement
return callback(null, false); // 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) { // If both are unconfirmed but replacement
self.isSpent(hash, i, function(err, spent) { // is older than spender, do nothing.
if (err) if (ref.ps < tx.ps)
return next(err); return callback();
if (spent) }
return self._removeSpenders(spent, ref, next);
next(); self._removeRecursive(tx, function(err, result, map) {
});
}, function(err) {
if (err) if (err)
return callback(err); return callback(err);
return self.lazyRemove(tx, function(err) { return callback(null, tx, map);
if (err)
return callback(err);
return callback(null, true);
}, true);
}); });
}); });
}; };
/**
* 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 * Test an entire transaction to see
* if any of its outpoints are a double-spend. * 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) { TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) {
var self = this; var self = this;
utils.everySerial(tx.inputs, function(input, next) { utils.everySerial(tx.inputs, function(input, next) {
self.isSpent(input.prevout.hash, input.prevout.index, function(err, spent) { self.isSpent(input.prevout.hash, input.prevout.index, function(err, spent) {
if (err) if (err)
@ -676,30 +695,33 @@ TXDB.prototype.isSpent = function isSpent(hash, index, callback) {
TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
var self = this; var self = this;
var hash = tx.hash('hex'); var hash, batch, unlock, i, path, id;
var batch;
unlock = this._lock(_confirm, [tx, map, callback], force);
var unlock = this._lock(_confirm, [tx, map, callback], force);
if (!unlock) if (!unlock)
return; return;
callback = utils.wrap(callback, unlock); callback = utils.wrap(callback, unlock);
hash = tx.hash('hex');
this.getTX(hash, function(err, existing) { this.getTX(hash, function(err, existing) {
if (err) if (err)
return callback(err); return callback(err);
// Haven't seen this tx before, add it. // Haven't seen this tx before, add it.
if (!existing) if (!existing)
return callback(null, false); return callback(null, false, map);
// Existing tx is already confirmed. Ignore. // Existing tx is already confirmed. Ignore.
if (existing.ts !== 0) 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) if (tx.ts === 0)
return callback(null, true); return callback(null, true, map);
batch = self.db.batch(); 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.del('m/' + pad32(existing.ps) + '/' + hash);
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
map.all.forEach(function(path) { for (i = 0; i < map.all.length; i++) {
var id = path.id + '/' + path.account; path = map.all[i];
id = path.id + '/' + path.account;
batch.del('P/' + id + '/' + hash); batch.del('P/' + id + '/' + hash);
batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash); batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash);
batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY);
}); }
utils.forEachSerial(tx.outputs, function(output, next, i) { utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash(); var address = output.getHash();
@ -758,7 +781,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
self.emit('confirmed', tx, map); self.emit('confirmed', tx, map);
self.emit('tx', 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) { TXDB.prototype.remove = function remove(hash, callback, force) {
var self = this; var self = this;
if (Array.isArray(hash)) {
return utils.forEachSerial(hash, function(hash, next) {
self.remove(hash, next, force);
}, callback);
}
if (hash.hash) if (hash.hash)
hash = hash.hash('hex'); hash = hash.hash('hex');
@ -815,12 +832,6 @@ TXDB.prototype.remove = function remove(hash, callback, force) {
TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) {
var self = this; 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) { return this.getMap(tx, function(err, map) {
if (err) if (err)
return callback(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) { TXDB.prototype._remove = function remove(tx, map, callback, force) {
var self = this; var self = this;
var hash = tx.hash('hex'); var unlock, hash, batch, i, j, path, id, key, paths, address, input, output;
var batch;
unlock = this._lock(remove, [tx, map, callback], force);
var unlock = this._lock(remove, [tx, map, callback], force);
if (!unlock) if (!unlock)
return; return;
callback = utils.wrap(callback, unlock); callback = utils.wrap(callback, unlock);
hash = tx.hash('hex');
batch = this.db.batch(); batch = this.db.batch();
batch.del('t/' + hash); batch.del('t/' + hash);
@ -863,8 +876,9 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
batch.del('m/' + pad32(tx.ts) + '/' + hash); batch.del('m/' + pad32(tx.ts) + '/' + hash);
} }
map.all.forEach(function(path) { for (i = 0; i < map.all.length; i++) {
var id = path.id + '/' + path.account; path = map.all[i];
id = path.id + '/' + path.account;
batch.del('T/' + id + '/' + hash); batch.del('T/' + id + '/' + hash);
if (tx.ts === 0) { if (tx.ts === 0) {
batch.del('P/' + id + '/' + hash); 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('H/' + id + '/' + pad32(tx.height) + '/' + hash);
batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash); batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash);
} }
}); }
this.fillHistory(tx, function(err) { this.fillHistory(tx, function(err) {
if (err) if (err)
return callback(err); return callback(err);
tx.inputs.forEach(function(input) { for (i = 0; i < tx.inputs.length; i++) {
var key = input.prevout.hash + '/' + input.prevout.index; input = tx.inputs[i];
var address = input.getHash(); key = input.prevout.hash + '/' + input.prevout.index;
address = input.getHash();
if (tx.isCoinbase()) if (tx.isCoinbase())
return; break;
if (!input.coin) if (!input.coin)
return; continue;
if (!address || !map.table[address].length) if (!address || !map.table[address].length)
return; continue;
if (address) { paths = map.table[address];
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account; for (j = 0; j < paths.length; j++) {
batch.put('C/' + id + '/' + key, DUMMY); path = paths[j];
}); id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
} }
batch.put('c/' + key, input.coin.toRaw()); batch.put('c/' + key, input.coin.toRaw());
batch.del('s/' + key); batch.del('s/' + key);
batch.del('o/' + key); batch.del('o/' + key);
}); }
tx.outputs.forEach(function(output, i) { for (i = 0; i < tx.outputs.length; i++) {
var key = hash + '/' + i; output = tx.outputs[i];
var address = output.getHash(); key = hash + '/' + i;
address = output.getHash();
if (!address || !map.table[address].length) if (!address || !map.table[address].length)
return; continue;
if (output.script.isUnspendable()) if (output.script.isUnspendable())
return; continue;
if (address) { paths = map.table[address];
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account; for (j = 0; j < paths.length; j++) {
batch.del('C/' + id + '/' + key); path = paths[j];
}); id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
} }
batch.del('c/' + key); batch.del('c/' + key);
}); }
batch.write(function(err) { batch.write(function(err) {
if (err) if (err)
@ -930,7 +948,7 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
self.emit('remove tx', tx, map); 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) { TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) {
var self = this; var self = this;
if (Array.isArray(hash)) {
return utils.forEachSerial(hash, function(hash, next) {
self.unconfirm(hash, next, force);
}, callback);
}
if (hash.hash) if (hash.hash)
hash = hash.hash('hex'); 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) { TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
var self = this; 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) if (!unlock)
return; return;
callback = utils.wrap(callback, unlock); callback = utils.wrap(callback, unlock);
hash = tx.hash('hex'); hash = tx.hash('hex');
batch = this.db.batch();
height = tx.height; height = tx.height;
ts = tx.ts; ts = tx.ts;
batch = this.db.batch();
if (height !== -1) if (height !== -1)
return callback(null, false); return callback(null, false, map);
tx.height = -1; tx.height = -1;
tx.ps = utils.now(); tx.ps = utils.now();
@ -1014,13 +1028,14 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
batch.del('m/' + pad32(ts) + '/' + hash); batch.del('m/' + pad32(ts) + '/' + hash);
batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
map.all.forEach(function(path) { for (i = 0; i < map.all.length; i++) {
var id = path.id + '/' + path.account; path = map.all[i];
id = path.id + '/' + path.account;
batch.put('P/' + id + '/' + hash, DUMMY); batch.put('P/' + id + '/' + hash, DUMMY);
batch.del('H/' + id + '/' + pad32(height) + '/' + hash); batch.del('H/' + id + '/' + pad32(height) + '/' + hash);
batch.del('M/' + id + '/' + pad32(ts) + '/' + hash); batch.del('M/' + id + '/' + pad32(ts) + '/' + hash);
batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY);
}); }
utils.forEachSerial(tx.outputs, function(output, next, i) { utils.forEachSerial(tx.outputs, function(output, next, i) {
self.getCoin(hash, i, function(err, coin) { 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); 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) { TXDB.prototype.zap = function zap(id, age, callback, force) {
var self = this; var self = this;
var unlock;
if (typeof age === 'function') { if (typeof age === 'function') {
force = callback; force = callback;
@ -1615,7 +1631,8 @@ TXDB.prototype.zap = function zap(id, age, callback, force) {
id = null; id = null;
} }
var unlock = this._lock(zap, [id, age, callback], force); unlock = this._lock(zap, [id, age, callback], force);
if (!unlock) if (!unlock)
return; return;

View File

@ -2243,10 +2243,10 @@ utils.pad32 = function pad32(num) {
*/ */
utils.wrap = function wrap(callback, unlock) { utils.wrap = function wrap(callback, unlock) {
return function(err, result) { return function(err, res1, res2) {
unlock(); unlock();
if (callback) if (callback)
callback(err, result); callback(err, res1, res2);
}; };
}; };

View File

@ -202,6 +202,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) {
} }
unlock = this.locker.lock(addKey, [account, key, callback]); unlock = this.locker.lock(addKey, [account, key, callback]);
if (!unlock) if (!unlock)
return; return;
@ -235,6 +236,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) {
} }
unlock = this.locker.lock(removeKey, [account, key, callback]); unlock = this.locker.lock(removeKey, [account, key, callback]);
if (!unlock) if (!unlock)
return; return;
@ -339,6 +341,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force
var master, key, unlock; var master, key, unlock;
unlock = this.locker.lock(createAccount, [options, callback], force); unlock = this.locker.lock(createAccount, [options, callback], force);
if (!unlock) if (!unlock)
return; return;
@ -396,6 +399,7 @@ Wallet.prototype.getAccounts = function getAccounts(callback) {
Wallet.prototype.getAccount = function getAccount(account, callback, force) { Wallet.prototype.getAccount = function getAccount(account, callback, force) {
var unlock = this.locker.lock(getAccount, [account, callback], force); var unlock = this.locker.lock(getAccount, [account, callback], force);
if (!unlock) if (!unlock)
return; return;
@ -454,6 +458,7 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac
} }
unlock = this.locker.lock(createAddress, [account, change, callback]); unlock = this.locker.lock(createAddress, [account, change, callback]);
if (!unlock) if (!unlock)
return; return;
@ -790,6 +795,7 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) {
var i, path, unlock; var i, path, unlock;
unlock = this.locker.lock(syncOutputDepth, [tx, callback]); unlock = this.locker.lock(syncOutputDepth, [tx, callback]);
if (!unlock) if (!unlock)
return; return;
@ -911,6 +917,7 @@ Wallet.prototype.scan = function scan(getByAddress, callback) {
var unlock; var unlock;
unlock = this.locker.lock(scan, [getByAddress, callback]); unlock = this.locker.lock(scan, [getByAddress, callback]);
if (!unlock) if (!unlock)
return; return;

View File

@ -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) { this.tx.on('confirmed', function(tx, map) {
self.emit('confirmed', tx, map); self.emit('confirmed', tx, map);
map.all.forEach(function(path) { map.all.forEach(function(path) {

View File

@ -192,7 +192,6 @@ describe('Wallet', function() {
var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000);
t1.addInput(dummyInput); t1.addInput(dummyInput);
// balance: 51000 // balance: 51000
// w.sign(t1);
w.sign(t1, function(err) { w.sign(t1, function(err) {
assert.ifError(err); assert.ifError(err);
var t2 = bcoin.mtx().addInput(t1, 0) // 50000 var t2 = bcoin.mtx().addInput(t1, 0) // 50000
@ -200,14 +199,12 @@ describe('Wallet', function() {
.addOutput(w, 24000); .addOutput(w, 24000);
di = t2.inputs[0]; di = t2.inputs[0];
// balance: 49000 // balance: 49000
// w.sign(t2);
w.sign(t2, function(err) { w.sign(t2, function(err) {
assert.ifError(err); assert.ifError(err);
var t3 = bcoin.mtx().addInput(t1, 1) // 1000 var t3 = bcoin.mtx().addInput(t1, 1) // 1000
.addInput(t2, 0) // 24000 .addInput(t2, 0) // 24000
.addOutput(w, 23000); .addOutput(w, 23000);
// balance: 47000 // balance: 47000
// w.sign(t3);
w.sign(t3, function(err) { w.sign(t3, function(err) {
assert.ifError(err); assert.ifError(err);
var t4 = bcoin.mtx().addInput(t2, 1) // 24000 var t4 = bcoin.mtx().addInput(t2, 1) // 24000
@ -215,19 +212,16 @@ describe('Wallet', function() {
.addOutput(w, 11000) .addOutput(w, 11000)
.addOutput(w, 11000); .addOutput(w, 11000);
// balance: 22000 // balance: 22000
// w.sign(t4);
w.sign(t4, function(err) { w.sign(t4, function(err) {
assert.ifError(err); assert.ifError(err);
var f1 = bcoin.mtx().addInput(t4, 1) // 11000 var f1 = bcoin.mtx().addInput(t4, 1) // 11000
.addOutput(f, 10000); .addOutput(f, 10000);
// balance: 11000 // balance: 11000
// w.sign(f1);
w.sign(f1, function(err) { w.sign(f1, function(err) {
assert.ifError(err); assert.ifError(err);
var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed)
.addOutput(w, 500); .addOutput(w, 500);
// Script inputs but do not sign // Script inputs but do not sign
// w.scriptInputs(fake);
w.scriptInputs(fake, function(err) { w.scriptInputs(fake, function(err) {
assert.ifError(err); assert.ifError(err);
// Fake signature // Fake signature
@ -300,13 +294,36 @@ describe('Wallet', function() {
it('should cleanup spenders after double-spend', function(cb) { it('should cleanup spenders after double-spend', function(cb) {
var t1 = bcoin.mtx().addOutput(dw, 5000); var t1 = bcoin.mtx().addOutput(dw, 5000);
t1.addInput(di); t1.addInput(di.coin);
walletdb.addTX(t1, function(err) { dw.getHistory(function(err, txs) {
assert.ifError(err); 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.ifError(err);
assert.equal(balance.total, 11000); dw.getBalance(function(err, balance) {
cb(); 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();
});
});
});
});
}); });
}); });
}); });