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) {
var self = this;
var iter;
if (!this.filter)
return callback();
iter = this.db.iterator({
this.db.iterate({
gte: 'W',
lte: 'W~',
keys: true,
values: false,
fillCache: false,
keyAsBuffer: false
});
(function next() {
iter.next(function(err, key, value) {
if (err) {
return iter.end(function() {
callback(err);
});
}
if (key === undefined)
return iter.end(callback);
transform: function(key) {
key = key.split('/')[1];
self.filter.add(key, 'hex');
next();
});
})();
}
}, callback);
};
TXDB.prototype._testFilter = function _testFilter(addresses) {
@ -297,12 +277,6 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) {
TXDB.prototype.add = function add(tx, callback, force) {
var self = this;
if (Array.isArray(tx)) {
return utils.forEachSerial(tx, function(tx, next) {
self.add(tx, next, force);
}, callback);
}
return this.getMap(tx, function(err, map) {
if (err)
return callback(err);
@ -314,18 +288,13 @@ TXDB.prototype.add = function add(tx, callback, force) {
});
};
// This big scary function is what a persistent tx pool
// looks like. It's a semi mempool in that it can handle
// receiving txs out of order.
TXDB.prototype._add = function add(tx, map, callback, force) {
var self = this;
var hash = tx.hash('hex');
var updated = false;
var batch;
var batch, hash, i, j, unlock, path, paths, id;
assert(tx.ts > 0 || tx.ps > 0);
unlock = this._lock(add, [tx, map, callback], force);
var unlock = this._lock(add, [tx, map, callback], force);
if (!unlock)
return;
@ -341,14 +310,15 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Ignore if we already have this tx.
if (existing)
return callback(null, true);
return callback(null, true, map);
hash = tx.hash('hex');
batch = self.db.batch();
batch.put('t/' + hash, tx.toExtended());
if (tx.ts === 0) {
assert(tx.ps > 0);
batch.put('p/' + hash, DUMMY);
batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
} else {
@ -356,8 +326,9 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
}
map.all.forEach(function(path) {
var id = path.id + '/' + path.account;
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
id = path.id + '/' + path.account;
batch.put('T/' + id + '/' + hash, DUMMY);
if (tx.ts === 0) {
batch.put('P/' + id + '/' + hash, DUMMY);
@ -366,12 +337,12 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY);
}
});
}
// Consume unspent money or add orphans
utils.forEachSerial(tx.inputs, function(input, next, i) {
var key, address;
var prevout = input.prevout;
var key, address;
if (tx.isCoinbase())
return next();
@ -388,6 +359,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
key = prevout.hash + '/' + prevout.index;
batch.put('s/' + key, tx.hash());
if (coin) {
// Add TX to inputs and spend money
input.coin = coin;
@ -400,28 +373,28 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
updated = true;
if (address) {
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
});
paths = map.table[address];
for (j = 0; j < paths.length; j++) {
path = paths[j];
id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
}
batch.del('c/' + key);
batch.put('s/' + key, tx.hash());
return next();
}
input.coin = null;
self.isSpent(prevout.hash, prevout.index, function(err, spentBy) {
self.isSpent(prevout.hash, prevout.index, function(err, spent) {
if (err)
return next(err);
// Are we double-spending?
// Replace older txs with newer ones.
if (spentBy) {
if (spent) {
return self.getTX(prevout.hash, function(err, prev) {
if (err)
return next(err);
@ -434,19 +407,22 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Skip invalid transactions
if (self.options.verify) {
if (!tx.verify(i))
return callback(null, false);
return callback(null, false, map);
}
return self._removeSpenders(spentBy, tx, function(err, result) {
return self._removeConflict(spent, tx, function(err, rtx, rmap) {
if (err)
return next(err);
if (!result) {
assert(tx.ts === 0, 'I\'m confused');
return callback(null, false);
}
// Spender was not removed, the current
// transaction is not elligible to be added.
if (!rtx)
return callback(null, false, map);
self.emit('conflict', rtx, rmap);
batch.clear();
self._add(tx, map, callback, true);
});
});
@ -470,14 +446,13 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Add unspent outputs or resolve orphans
utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash();
var key, coin;
var key = hash + '/' + i;
var coin;
// Do not add unspents for outputs that aren't ours.
if (!address || !map.table[address].length)
return next();
key = hash + '/' + i;
if (output.script.isUnspendable())
return next();
@ -518,11 +493,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
return next();
}
self.lazyRemove(orphan.tx, function(err) {
if (err)
return next(err);
return next();
}, true);
self.lazyRemove(orphan.tx, next, true);
}, function(err) {
if (err)
return next(err);
@ -538,11 +509,12 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
return next(err);
if (!orphans) {
if (address) {
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
});
paths = map.table[address];
for (j = 0; j < paths.length; j++) {
path = paths[j];
id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
}
batch.put('c/' + key, coin.toRaw());
@ -573,7 +545,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
self.emit('updated', tx, map);
}
return callback(null, true);
return callback(null, true, map);
});
});
});
@ -593,8 +565,9 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
* @param {Function} callback - Returns [Error, Boolean].
*/
TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) {
TXDB.prototype._removeConflict = function _removeConflict(hash, ref, callback) {
var self = this;
this.getTX(hash, function(err, tx) {
if (err)
return callback(err);
@ -602,32 +575,77 @@ TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) {
if (!tx)
return callback(new Error('Could not find spender.'));
if (tx.ts !== 0)
return callback(null, false);
if (tx.ts !== 0) {
// If spender is confirmed and replacement
// is not confirmed, do nothing.
if (ref.ts === 0)
return callback();
if (ref.ts === 0 && ref.ps < tx.ps)
return callback(null, false);
// If both are confirmed but replacement
// is older than spender, do nothing.
if (ref.ts < tx.ts)
return callback();
} else {
// If spender is unconfirmed and replacement
// is confirmed, do nothing.
if (ref.ts !== 0)
return callback();
utils.forEachSerial(tx.outputs, function(output, next, i) {
self.isSpent(hash, i, function(err, spent) {
if (err)
return next(err);
if (spent)
return self._removeSpenders(spent, ref, next);
next();
});
}, function(err) {
// If both are unconfirmed but replacement
// is older than spender, do nothing.
if (ref.ps < tx.ps)
return callback();
}
self._removeRecursive(tx, function(err, result, map) {
if (err)
return callback(err);
return self.lazyRemove(tx, function(err) {
if (err)
return callback(err);
return callback(null, true);
}, true);
return callback(null, tx, map);
});
});
};
/**
* Remove a transaction and recursively
* remove all of its spenders.
* @private
* @param {TX} tx - Transaction to be removed.
* @param {Function} callback - Returns [Error, Boolean].
*/
TXDB.prototype._removeRecursive = function _removeRecursive(tx, callback) {
var self = this;
var hash = tx.hash('hex');
utils.forEachSerial(tx.outputs, function(output, next, i) {
self.isSpent(hash, i, function(err, spent) {
if (err)
return next(err);
// Remove all of the spender's spenders first.
if (spent) {
return self.getTX(spent, function(err, tx) {
if (err)
return callback(err);
if (!tx)
return callback(new Error('Could not find spender.'));
return self._removeRecursive(tx, next);
});
}
next();
});
}, function(err) {
if (err)
return callback(err);
// Remove the spender.
return self.lazyRemove(tx, callback, true);
});
};
/**
* Test an entire transaction to see
* if any of its outpoints are a double-spend.
@ -637,6 +655,7 @@ TXDB.prototype._removeSpenders = function removeSpenders(hash, ref, callback) {
TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) {
var self = this;
utils.everySerial(tx.inputs, function(input, next) {
self.isSpent(input.prevout.hash, input.prevout.index, function(err, spent) {
if (err)
@ -676,30 +695,33 @@ TXDB.prototype.isSpent = function isSpent(hash, index, callback) {
TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
var self = this;
var hash = tx.hash('hex');
var batch;
var hash, batch, unlock, i, path, id;
unlock = this._lock(_confirm, [tx, map, callback], force);
var unlock = this._lock(_confirm, [tx, map, callback], force);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
hash = tx.hash('hex');
this.getTX(hash, function(err, existing) {
if (err)
return callback(err);
// Haven't seen this tx before, add it.
if (!existing)
return callback(null, false);
return callback(null, false, map);
// Existing tx is already confirmed. Ignore.
if (existing.ts !== 0)
return callback(null, true);
return callback(null, true, map);
// The incoming tx won't confirm the existing one anyway. Ignore.
// The incoming tx won't confirm the
// existing one anyway. Ignore.
if (tx.ts === 0)
return callback(null, true);
return callback(null, true, map);
batch = self.db.batch();
@ -715,13 +737,14 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
batch.del('m/' + pad32(existing.ps) + '/' + hash);
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
map.all.forEach(function(path) {
var id = path.id + '/' + path.account;
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
id = path.id + '/' + path.account;
batch.del('P/' + id + '/' + hash);
batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash);
batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY);
});
}
utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash();
@ -758,7 +781,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
self.emit('confirmed', tx, map);
self.emit('tx', tx, map);
return callback(null, true);
return callback(null, true, map);
});
});
});
@ -774,12 +797,6 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
TXDB.prototype.remove = function remove(hash, callback, force) {
var self = this;
if (Array.isArray(hash)) {
return utils.forEachSerial(hash, function(hash, next) {
self.remove(hash, next, force);
}, callback);
}
if (hash.hash)
hash = hash.hash('hex');
@ -815,12 +832,6 @@ TXDB.prototype.remove = function remove(hash, callback, force) {
TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) {
var self = this;
if (Array.isArray(tx)) {
return utils.forEachSerial(tx, function(tx, next) {
self.lazyRemove(tx, next, force);
}, callback);
}
return this.getMap(tx, function(err, map) {
if (err)
return callback(err);
@ -842,15 +853,17 @@ TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) {
TXDB.prototype._remove = function remove(tx, map, callback, force) {
var self = this;
var hash = tx.hash('hex');
var batch;
var unlock, hash, batch, i, j, path, id, key, paths, address, input, output;
unlock = this._lock(remove, [tx, map, callback], force);
var unlock = this._lock(remove, [tx, map, callback], force);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
hash = tx.hash('hex');
batch = this.db.batch();
batch.del('t/' + hash);
@ -863,8 +876,9 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
batch.del('m/' + pad32(tx.ts) + '/' + hash);
}
map.all.forEach(function(path) {
var id = path.id + '/' + path.account;
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
id = path.id + '/' + path.account;
batch.del('T/' + id + '/' + hash);
if (tx.ts === 0) {
batch.del('P/' + id + '/' + hash);
@ -873,56 +887,60 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash);
batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash);
}
});
}
this.fillHistory(tx, function(err) {
if (err)
return callback(err);
tx.inputs.forEach(function(input) {
var key = input.prevout.hash + '/' + input.prevout.index;
var address = input.getHash();
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
key = input.prevout.hash + '/' + input.prevout.index;
address = input.getHash();
if (tx.isCoinbase())
return;
break;
if (!input.coin)
return;
continue;
if (!address || !map.table[address].length)
return;
continue;
if (address) {
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
});
paths = map.table[address];
for (j = 0; j < paths.length; j++) {
path = paths[j];
id = path.id + '/' + path.account;
batch.put('C/' + id + '/' + key, DUMMY);
}
batch.put('c/' + key, input.coin.toRaw());
batch.del('s/' + key);
batch.del('o/' + key);
});
}
tx.outputs.forEach(function(output, i) {
var key = hash + '/' + i;
var address = output.getHash();
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
key = hash + '/' + i;
address = output.getHash();
if (!address || !map.table[address].length)
return;
continue;
if (output.script.isUnspendable())
return;
continue;
if (address) {
map.table[address].forEach(function(path) {
var id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
});
paths = map.table[address];
for (j = 0; j < paths.length; j++) {
path = paths[j];
id = path.id + '/' + path.account;
batch.del('C/' + id + '/' + key);
}
batch.del('c/' + key);
});
}
batch.write(function(err) {
if (err)
@ -930,7 +948,7 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
self.emit('remove tx', tx, map);
return callback(null, true);
return callback(null, true, map);
});
});
};
@ -944,12 +962,6 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) {
var self = this;
if (Array.isArray(hash)) {
return utils.forEachSerial(hash, function(hash, next) {
self.unconfirm(hash, next, force);
}, callback);
}
if (hash.hash)
hash = hash.hash('hex');
@ -985,21 +997,23 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) {
TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
var self = this;
var hash, batch, height, ts;
var batch, unlock, hash, height, ts, i, path, id;
unlock = this._lock(unconfirm, [tx, map, callback], force);
var unlock = this._lock(unconfirm, [tx, map, callback], force);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
hash = tx.hash('hex');
batch = this.db.batch();
height = tx.height;
ts = tx.ts;
batch = this.db.batch();
if (height !== -1)
return callback(null, false);
return callback(null, false, map);
tx.height = -1;
tx.ps = utils.now();
@ -1014,13 +1028,14 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
batch.del('m/' + pad32(ts) + '/' + hash);
batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
map.all.forEach(function(path) {
var id = path.id + '/' + path.account;
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
id = path.id + '/' + path.account;
batch.put('P/' + id + '/' + hash, DUMMY);
batch.del('H/' + id + '/' + pad32(height) + '/' + hash);
batch.del('M/' + id + '/' + pad32(ts) + '/' + hash);
batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY);
});
}
utils.forEachSerial(tx.outputs, function(output, next, i) {
self.getCoin(hash, i, function(err, coin) {
@ -1046,7 +1061,7 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
self.emit('unconfirmed', tx, map);
return callback(null, true);
return callback(null, true, map);
});
});
};
@ -1607,6 +1622,7 @@ TXDB.prototype.getBalance = function getBalance(id, callback) {
TXDB.prototype.zap = function zap(id, age, callback, force) {
var self = this;
var unlock;
if (typeof age === 'function') {
force = callback;
@ -1615,7 +1631,8 @@ TXDB.prototype.zap = function zap(id, age, callback, force) {
id = null;
}
var unlock = this._lock(zap, [id, age, callback], force);
unlock = this._lock(zap, [id, age, callback], force);
if (!unlock)
return;

View File

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

View File

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

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

View File

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