txdb: orphan resolution.

This commit is contained in:
Christopher Jeffrey 2016-10-15 21:02:40 -07:00
parent 212c1a3430
commit c07848fadd
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
9 changed files with 277 additions and 194 deletions

View File

@ -34,7 +34,9 @@ var walletdb = new bcoin.walletdb({
name: 'wallet-test', name: 'wallet-test',
// location: __dirname + '/../walletdb-bench', // location: __dirname + '/../walletdb-bench',
// db: 'leveldb' // db: 'leveldb'
db: 'memory' db: 'memory',
resolution: false,
verify: false
}); });
var runBench = co(function* runBench() { var runBench = co(function* runBench() {

View File

@ -148,6 +148,7 @@ function Fullnode(options) {
witness: this.options.witness, witness: this.options.witness,
useCheckpoints: this.options.useCheckpoints, useCheckpoints: this.options.useCheckpoints,
maxFiles: this.options.maxFiles, maxFiles: this.options.maxFiles,
resolution: true,
verify: false verify: false
}); });

View File

@ -86,6 +86,7 @@ function SPVNode(options) {
location: this.location('walletdb'), location: this.location('walletdb'),
witness: this.options.witness, witness: this.options.witness,
maxFiles: this.options.maxFiles, maxFiles: this.options.maxFiles,
resolution: true,
verify: true verify: true
}); });

View File

@ -120,12 +120,6 @@ layout.txdb = {
ss: function ss(key) { ss: function ss(key) {
return this.hii(key); return this.hii(key);
}, },
o: function o(hash, index) {
return this.hi('o', hash, index);
},
oo: function oo(key) {
return this.hii(key);
},
p: function p(hash) { p: function p(hash) {
return this.ha('p', hash); return this.ha('p', hash);
}, },

View File

@ -108,7 +108,9 @@ PathInfo.fromTX = function fromTX(wallet, tx, paths) {
* @returns {Boolean} * @returns {Boolean}
*/ */
PathInfo.prototype.hasPath = function hasPath(hash) { PathInfo.prototype.hasPath = function hasPath(output) {
var hash = output.getHash('hex');
if (!hash) if (!hash)
return false; return false;
@ -121,7 +123,9 @@ PathInfo.prototype.hasPath = function hasPath(hash) {
* @returns {Path} * @returns {Path}
*/ */
PathInfo.prototype.getPath = function getPath(hash) { PathInfo.prototype.getPath = function getPath(output) {
var hash = output.getHash('hex');
if (!hash) if (!hash)
return; return;

View File

@ -136,12 +136,6 @@ var layout = {
ss: function ss(key) { ss: function ss(key) {
return layout.hii(key); return layout.hii(key);
}, },
o: function o(hash, index) {
return layout.hi(0x6f, hash, index);
},
oo: function oo(key) {
return layout.hii(key);
},
p: function p(hash) { p: function p(hash) {
return layout.ha(0x70, hash); return layout.ha(0x70, hash);
}, },
@ -219,6 +213,10 @@ function TXDB(wallet) {
this.balance = null; this.balance = null;
this.pending = null; this.pending = null;
this.events = []; this.events = [];
this.orphans = {};
this.count = {};
this.totalOrphans = 0;
} }
/** /**
@ -438,57 +436,183 @@ TXDB.prototype.getPathInfo = function getPathInfo(tx) {
}; };
/** /**
* Add an orphan (tx hash + input index) * Determine which transactions to add.
* to orphan list. Stored by its required coin ID. * Attempt to resolve orphans (for SPV).
* @private * @param {TX} tx
* @param {Outpoint} prevout - Required coin hash & index. * @returns {Promise}
* @param {Buffer} input - Spender input hash and index.
* @returns {Promise} - Returns Buffer.
*/ */
TXDB.prototype.addOrphan = co(function* addOrphan(prevout, input) { TXDB.prototype.resolve = co(function* add(tx) {
var key = layout.o(prevout.hash, prevout.index); var hash, result;
var data = yield this.get(key);
var p = new BufferWriter();
if (data) if (!this.options.resolution)
p.writeBytes(data); return [tx];
p.writeBytes(input); hash = tx.hash('hex');
this.put(key, p.render()); if (yield this.hasTX(hash))
return [tx];
result = yield this.verifyInputs(tx);
if (!result)
return [];
return yield this.resolveOutputs(tx);
}); });
/** /**
* Retrieve orphan list by coin ID. * Verify inputs and potentially add orphans.
* @private * Used in SPV mode.
* @param {Hash} hash * @param {TX} tx
* @param {Number} index * @returns {Promise}
* @returns {Promise} - Returns {@link Orphan}.
*/ */
TXDB.prototype.getOrphans = co(function* getOrphans(hash, index) { TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) {
var key = layout.o(hash, index); var hash = tx.hash('hex');
var data = yield this.get(key); var hasOrphans = false;
var items = []; var orphans = [];
var i, inputs, input, tx, p; var i, input, prevout, address;
var path, key, coin, spent;
if (!data) if (tx.isCoinbase())
return; return true;
p = new BufferReader(data); if (this.count[hash])
inputs = []; return false;
while (p.left()) for (i = 0; i < tx.inputs.length; i++) {
inputs.push(Outpoint.fromRaw(p)); input = tx.inputs[i];
prevout = input.prevout;
coin = yield this.getCoin(prevout.hash, prevout.index);
for (i = 0; i < inputs.length; i++) { if (coin) {
input = inputs[i]; input.coin = coin;
tx = yield this.getTX(input.hash);
items.push(new Orphan(input, tx)); if (this.options.verify) {
if (!(yield tx.verifyInputAsync(i)))
return false;
}
continue;
}
spent = yield this.isSpent(prevout.hash, prevout.index);
if (!spent) {
address = input.getHash('hex');
path = yield this.wallet.hasPath(address);
if (!path)
continue;
orphans[i] = true;
continue;
}
coin = yield this.getSpentCoin(spent, prevout);
assert(coin);
input.coin = coin;
if (this.options.verify) {
if (!(yield tx.verifyInputAsync(i)))
return false;
}
} }
return items; for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
prevout = input.prevout;
if (!orphans[i])
continue;
key = prevout.hash + prevout.index;
if (this.totalOrphans > 20) {
this.logger.warning('Potential orphan flood!');
this.logger.warning(
'More than 20 orphans for %s. Purging.',
this.wallet.id);
this.totalOrphans = 0;
this.orphans = {};
this.count = {};
}
if (!this.orphans[key])
this.orphans[key] = [];
if (!this.count[hash])
this.count[hash] = 0;
this.orphans[key].push(new Orphan(tx, i));
this.count[hash]++;
this.totalOrphans++;
hasOrphans = true;
}
if (hasOrphans)
return false;
return true;
});
/**
* Resolve orphans for outputs.
* Used in SPV mode.
* @param {TX} tx
* @returns {Promise}
*/
TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) {
var hash = tx.hash('hex');
var i, input, output, key, orphans, orphan, coin, valid;
if (!resolved)
resolved = [];
resolved.push(tx);
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
key = hash + i;
orphans = this.orphans[key];
if (!orphans)
continue;
delete this.orphans[key];
coin = Coin.fromTX(tx, i);
while (orphans.length) {
orphan = orphans.pop();
valid = true;
input = orphan.tx.inputs[orphan.index];
input.coin = coin;
assert(input.prevout.hash === hash);
assert(input.prevout.index === i);
if (this.options.verify)
valid = yield orphan.tx.verifyInputAsync(orphan.index);
if (valid) {
if (--this.count[orphan.hash] === 0) {
delete this.count[orphan.hash];
yield this.resolveOutputs(orphan.tx, resolved);
}
break;
}
delete this.count[orphan.hash];
}
}
return resolved;
}); });
/** /**
@ -500,63 +624,48 @@ TXDB.prototype.getOrphans = co(function* getOrphans(hash, index) {
* @returns {Promise} * @returns {Promise}
*/ */
TXDB.prototype.verify = co(function* verify(tx, info) { TXDB.prototype.getInputs = co(function* getInputs(tx, info) {
var spends = []; var spends = [];
var orphans = []; var coins = [];
var removed = {}; var removed = {};
var i, input, prevout, address, coin, spent, conflict; var i, input, prevout, coin, spent, conflict;
if (tx.isCoinbase()) if (tx.isCoinbase())
return orphans; return coins;
for (i = 0; i < tx.inputs.length; i++) { for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i]; input = tx.inputs[i];
prevout = input.prevout; prevout = input.prevout;
address = input.getHash('hex');
// Only bother if this input is ours.
if (!info.hasPath(address))
continue;
// Try to get the coin we're redeeming.
coin = yield this.getCoin(prevout.hash, prevout.index); coin = yield this.getCoin(prevout.hash, prevout.index);
if (coin) { if (coin) {
// Add TX to inputs and spend money
input.coin = coin; input.coin = coin;
coins.push(coin);
// Skip invalid transactions
if (this.options.verify) {
if (!(yield tx.verifyInputAsync(i)))
return;
}
continue; continue;
} }
// Is it already spent?
spent = yield this.isSpent(prevout.hash, prevout.index); spent = yield this.isSpent(prevout.hash, prevout.index);
// Orphan until we see a parent transaction. // If we have no coin or spend
// record, this is not our input.
// This is assuming everything came
// in order!
if (!spent) { if (!spent) {
orphans[i] = true; coins.push(null);
continue; continue;
} }
// We must be double-spending. // Yikes, we're double spending. We
// still need the coin for after we
// resolve the conflict.
coin = yield this.getSpentCoin(spent, prevout); coin = yield this.getSpentCoin(spent, prevout);
assert(coin);
// Double-spent orphan.
if (!coin) {
orphans[i] = true;
continue;
}
input.coin = coin; input.coin = coin;
coins.push(coin);
// Skip invalid transactions
if (this.options.verify) {
if (!(yield tx.verifyInputAsync(i)))
return;
}
spends[i] = spent; spends[i] = spent;
} }
@ -598,66 +707,7 @@ TXDB.prototype.verify = co(function* verify(tx, info) {
this.emit('conflict', conflict.tx, conflict.info); this.emit('conflict', conflict.tx, conflict.info);
} }
return orphans; return coins;
});
/**
* Attempt to resolve orphans for an output.
* @private
* @param {TX} tx
* @param {Number} index
* @returns {Promise}
*/
TXDB.prototype.resolveOrphans = co(function* resolveOrphans(tx, index) {
var hash = tx.hash('hex');
var i, orphans, coin, input, spender, orphan;
orphans = yield this.getOrphans(hash, index);
if (!orphans)
return false;
this.del(layout.o(hash, index));
coin = Coin.fromTX(tx, index);
// Add input to resolved orphan.
for (i = 0; i < orphans.length; i++) {
orphan = orphans[i];
spender = orphan.input;
tx = orphan.tx;
// Probably removed by some other means.
if (!tx)
continue;
input = tx.inputs[spender.index];
input.coin = coin;
assert(input.prevout.hash === hash);
assert(input.prevout.index === index);
// Verify that input script is correct, if not - add
// output to unspent and remove orphan from storage
if (!this.options.verify || (yield tx.verifyInputAsync(spender.index))) {
// Add the undo coin record which we never had.
this.put(layout.d(spender.hash, spender.index), coin.toRaw());
// Add the spender record back in case any evil
// transactions were removed with lazyRemove.
this.put(layout.s(hash, index), spender.toRaw());
return true;
}
yield this.lazyRemove(tx);
}
// We had orphans, but they were invalid. The
// balance will be (incorrectly) added outside.
// Subtract to compensate.
this.pending.sub(coin);
return false;
}); });
/** /**
@ -696,7 +746,7 @@ TXDB.prototype.add = co(function* add(tx) {
TXDB.prototype._add = co(function* add(tx, info) { TXDB.prototype._add = co(function* add(tx, info) {
var hash, path, account; var hash, path, account;
var i, result, input, output, coin; var i, result, input, output, coin;
var prevout, key, address, spender, orphans; var prevout, key, spender, coins;
assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); assert(!tx.mutable, 'Cannot add mutable TX to wallet.');
@ -709,9 +759,9 @@ TXDB.prototype._add = co(function* add(tx, info) {
// Verify and get coins. // Verify and get coins.
// This potentially removes double-spenders. // This potentially removes double-spenders.
orphans = yield this.verify(tx, info); coins = yield this.getInputs(tx, info);
if (!orphans) if (!coins)
return false; return false;
hash = tx.hash('hex'); hash = tx.hash('hex');
@ -743,56 +793,41 @@ TXDB.prototype._add = co(function* add(tx, info) {
for (i = 0; i < tx.inputs.length; i++) { for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i]; input = tx.inputs[i];
prevout = input.prevout; prevout = input.prevout;
coin = coins[i];
address = input.getHash('hex');
path = info.getPath(address);
// Only bother if this input is ours. // Only bother if this input is ours.
if (!path) if (!coin)
continue; continue;
path = info.getPath(coin);
assert(path);
key = prevout.hash + prevout.index; key = prevout.hash + prevout.index;
// s[outpoint-key] -> [spender-hash]|[spender-input-index] // s[outpoint-key] -> [spender-hash]|[spender-input-index]
spender = Outpoint.fromTX(tx, i).toRaw(); spender = Outpoint.fromTX(tx, i).toRaw();
this.put(layout.s(prevout.hash, prevout.index), spender); this.put(layout.s(prevout.hash, prevout.index), spender);
// Add orphan if no parent transaction known.
// Do not disconnect any coins.
if (orphans[i]) {
yield this.addOrphan(prevout, spender);
continue;
}
this.del(layout.c(prevout.hash, prevout.index)); this.del(layout.c(prevout.hash, prevout.index));
this.del(layout.C(path.account, prevout.hash, prevout.index)); this.del(layout.C(path.account, prevout.hash, prevout.index));
this.put(layout.d(hash, i), input.coin.toRaw()); this.put(layout.d(hash, i), coin.toRaw());
this.pending.sub(input.coin); this.pending.sub(coin);
this.coinCache.remove(key); this.coinCache.remove(key);
} }
} }
// Add unspent outputs or resolve orphans // Add unspent outputs or resolve orphans.
for (i = 0; i < tx.outputs.length; i++) { for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i]; output = tx.outputs[i];
address = output.getHash('hex'); path = info.getPath(output);
key = hash + i; key = hash + i;
path = info.getPath(address); // Do not add unspents for
// outputs that aren't ours.
// Do not add unspents for outputs that aren't ours.
if (!path) if (!path)
continue; continue;
orphans = yield this.resolveOrphans(tx, i);
// If this transaction resolves an orphan,
// it should not connect coins as they are
// already spent by the orphan it resolved.
if (orphans)
continue;
coin = Coin.fromTX(tx, i); coin = Coin.fromTX(tx, i);
this.pending.add(coin); this.pending.add(coin);
@ -840,10 +875,10 @@ TXDB.prototype.removeConflict = co(function* removeConflict(hash, ref, removed)
if (!tx) if (!tx)
throw new Error('Could not find spender.'); throw new Error('Could not find spender.');
if (tx.ts !== 0) { if (tx.height !== -1) {
// If spender is confirmed and replacement // If spender is confirmed and replacement
// is not confirmed, do nothing. // is not confirmed, do nothing.
if (ref.ts === 0) if (ref.height === -1)
return; return;
// If both are confirmed but replacement // If both are confirmed but replacement
@ -853,13 +888,12 @@ TXDB.prototype.removeConflict = co(function* removeConflict(hash, ref, removed)
} else { } else {
// If spender is unconfirmed and replacement // If spender is unconfirmed and replacement
// is confirmed, do nothing. // is confirmed, do nothing.
if (ref.ts !== 0) if (ref.height === -1) {
return; // If both are unconfirmed but replacement
// is older than spender, do nothing.
// If both are unconfirmed but replacement if (ref.ps < tx.ps)
// is older than spender, do nothing. return;
if (ref.ps < tx.ps) }
return;
} }
info = yield this.removeRecursive(tx, removed); info = yield this.removeRecursive(tx, removed);
@ -1002,11 +1036,10 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
for (i = 0; i < tx.outputs.length; i++) { for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i]; output = tx.outputs[i];
address = output.getHash('hex');
key = hash + i; key = hash + i;
// Only update coins if this output is ours. // Only update coins if this output is ours.
if (!info.hasPath(address)) if (!info.hasPath(output))
continue; continue;
coin = yield this.getCoin(hash, i); coin = yield this.getCoin(hash, i);
@ -1108,7 +1141,7 @@ TXDB.prototype.lazyRemove = co(function* lazyRemove(tx) {
TXDB.prototype.__remove = co(function* remove(tx, info) { TXDB.prototype.__remove = co(function* remove(tx, info) {
var hash = tx.hash('hex'); var hash = tx.hash('hex');
var i, path, account, key, prevout; var i, path, account, key, prevout;
var address, input, output, coin; var input, output, coin;
this.del(layout.t(hash)); this.del(layout.t(hash));
@ -1139,12 +1172,11 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
input = tx.inputs[i]; input = tx.inputs[i];
key = input.prevout.hash + input.prevout.index; key = input.prevout.hash + input.prevout.index;
prevout = input.prevout; prevout = input.prevout;
address = input.getHash('hex');
if (!input.coin) if (!input.coin)
continue; continue;
path = info.getPath(address); path = info.getPath(input.coin);
if (!path) if (!path)
continue; continue;
@ -1157,7 +1189,6 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY);
this.del(layout.d(hash, i)); this.del(layout.d(hash, i));
this.del(layout.s(prevout.hash, prevout.index)); this.del(layout.s(prevout.hash, prevout.index));
this.del(layout.o(prevout.hash, prevout.index));
this.coinCache.set(key, coin); this.coinCache.set(key, coin);
} }
@ -1166,9 +1197,7 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
for (i = 0; i < tx.outputs.length; i++) { for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i]; output = tx.outputs[i];
key = hash + i; key = hash + i;
address = output.getHash('hex'); path = info.getPath(output);
path = info.getPath(address);
if (!path) if (!path)
continue; continue;
@ -2406,9 +2435,10 @@ function Conflict(tx, info) {
this.info = info; this.info = info;
} }
function Orphan(input, tx) { function Orphan(tx, i) {
this.input = input;
this.tx = tx; this.tx = tx;
this.hash = tx.hash('hex');
this.index = i;
} }
/* /*

View File

@ -1092,6 +1092,24 @@ Wallet.prototype.getPath = co(function* getPath(address) {
return path; return path;
}); });
/**
* Test whether the wallet contains a path.
* @param {Address|Hash} address
* @returns {Promise} - Returns {Boolean}.
*/
Wallet.prototype.hasPath = co(function* hasPath(address) {
var hash = Address.getHash(address, 'hex');
if (!hash)
return false;
if (this.pathCache.has(hash))
return true;
return yield this.db.hasPath(this.wid, hash);
});
/** /**
* Get all wallet paths. * Get all wallet paths.
* @param {(String|Number)?} acct * @param {(String|Number)?} acct
@ -1802,11 +1820,28 @@ Wallet.prototype.add = co(function* add(tx) {
/** /**
* Add a transaction to the wallet without a lock. * Add a transaction to the wallet without a lock.
* Potentially resolves orphans.
* @private
* @param {TX} tx * @param {TX} tx
* @returns {Promise} * @returns {Promise}
*/ */
Wallet.prototype._add = co(function* add(tx) { Wallet.prototype._add = co(function* add(tx) {
var resolved = yield this.txdb.resolve(tx);
var i;
for (i = 0; i < resolved.length; i++)
yield this._insert(resolved[i]);
});
/**
* Insert a transaction into the wallet (no lock).
* @private
* @param {TX} tx
* @returns {Promise}
*/
Wallet.prototype._insert = co(function* insert(tx) {
var info = yield this.getPathInfo(tx); var info = yield this.getPathInfo(tx);
var result, derived; var result, derived;

View File

@ -876,6 +876,18 @@ WalletDB.prototype.getPath = co(function* getPath(wid, hash) {
return path; return path;
}); });
/**
* Test whether a wallet contains a path.
* @param {WalletID} wid
* @param {Hash} hash
* @returns {Promise}
*/
WalletDB.prototype.hasPath = co(function* hasPath(wid, hash) {
var data = yield this.db.get(layout.P(wid, hash));
return data != null;
});
/** /**
* Get all address hashes. * Get all address hashes.
* @returns {Promise} * @returns {Promise}

View File

@ -42,6 +42,7 @@ describe('Wallet', function() {
walletdb = new bcoin.walletdb({ walletdb = new bcoin.walletdb({
name: 'wallet-test', name: 'wallet-test',
db: 'memory', db: 'memory',
resolution: true,
verify: true verify: true
}); });
@ -244,17 +245,20 @@ describe('Wallet', function() {
yield walletdb.addTX(t4); yield walletdb.addTX(t4);
balance = yield w.getBalance(); balance = yield w.getBalance();
assert.equal(balance.total, 22500); //assert.equal(balance.total, 22500);
assert.equal(balance.total, 0);
yield walletdb.addTX(t1); yield walletdb.addTX(t1);
balance = yield w.getBalance(); balance = yield w.getBalance();
assert.equal(balance.total, 73000); //assert.equal(balance.total, 73000);
assert.equal(balance.total, 51000);
yield walletdb.addTX(t2); yield walletdb.addTX(t2);
balance = yield w.getBalance(); balance = yield w.getBalance();
assert.equal(balance.total, 47000); //assert.equal(balance.total, 47000);
assert.equal(balance.total, 49000);
yield walletdb.addTX(t3); yield walletdb.addTX(t3);