wallet: share batches between wallet and txdb.

This commit is contained in:
Christopher Jeffrey 2016-10-04 00:33:07 -07:00
parent a800f8c44b
commit 08c7136ef4
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 260 additions and 297 deletions

View File

@ -1,3 +1,11 @@
/*!
* pathinfo.js - pathinfo object for bcoin
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
var utils = require('../utils/utils');
/**
@ -53,7 +61,7 @@ function PathInfo(wallet, tx, paths) {
PathInfo.prototype.fromTX = function fromTX(tx, paths) {
var uniq = {};
var i, j, hashes, hash, paths, path;
var i, hashes, hash, path;
this.tx = tx;
@ -72,8 +80,9 @@ PathInfo.prototype.fromTX = function fromTX(tx, paths) {
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
paths = this.pathMap[hash];
this.paths.push(path);
path = this.pathMap[hash];
if (path)
this.paths.push(path);
}
return this;
@ -151,8 +160,6 @@ PathInfo.prototype.toJSON = function toJSON() {
return json;
};
module.exports = PathInfo;
/**
* Transaction Details
* @constructor
@ -203,7 +210,7 @@ Details.prototype.init = function init(map) {
*/
Details.prototype._insert = function _insert(vector, input, target, map) {
var i, j, io, address, hash, paths, path, member;
var i, io, address, hash, path, member;
for (i = 0; i < vector.length; i++) {
io = vector[i];
@ -294,3 +301,9 @@ DetailsMember.prototype.toJSON = function toJSON(network) {
: null
};
};
/*
* Expose
*/
module.exports = PathInfo;

View File

@ -8,7 +8,6 @@
'use strict';
var utils = require('../utils/utils');
var Locker = require('../utils/locker');
var LRU = require('../utils/lru');
var co = require('../utils/co');
var assert = require('assert');
@ -214,10 +213,7 @@ function TXDB(wallet) {
this.options = wallet.db.options;
this.locked = {};
this.locker = new Locker();
this.coinCache = new LRU(10000);
this.current = null;
this.balance = null;
}
@ -267,17 +263,6 @@ TXDB.prototype.prefix = function prefix(key) {
return layout.prefix(this.wallet.wid, key);
};
/**
* Start a batch.
* @returns {Batch}
*/
TXDB.prototype.start = function start() {
assert(!this.current);
this.current = this.db.batch();
return this.current;
};
/**
* Put key and value to current batch.
* @param {String} key
@ -285,8 +270,8 @@ TXDB.prototype.start = function start() {
*/
TXDB.prototype.put = function put(key, value) {
assert(this.current);
this.current.put(this.prefix(key), value);
assert(this.wallet.current);
this.wallet.current.put(this.prefix(key), value);
};
/**
@ -295,29 +280,8 @@ TXDB.prototype.put = function put(key, value) {
*/
TXDB.prototype.del = function del(key) {
assert(this.current);
this.current.del(this.prefix(key));
};
/**
* Get current batch.
* @returns {Batch}
*/
TXDB.prototype.batch = function batch() {
assert(this.current);
return this.current;
};
/**
* Drop current batch.
* @returns {Batch}
*/
TXDB.prototype.drop = function drop() {
assert(this.current);
this.current.clear();
this.current = null;
assert(this.wallet.current);
this.wallet.current.del(this.prefix(key));
};
/**
@ -380,29 +344,13 @@ TXDB.prototype.values = function values(options) {
return this.db.values(options);
};
/**
* Commit current batch.
* @returns {Promise}
*/
TXDB.prototype.commit = co(function* commit() {
assert(this.current);
try {
yield this.current.write();
} catch (e) {
this.current = null;
throw e;
}
this.current = null;
});
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
* @returns {Promise} - Returns {@link PathInfo}.
*/
TXDB.prototype.getInfo = function getInfo(tx) {
TXDB.prototype.getPathInfo = function getPathInfo(tx) {
return this.wallet.getPathInfo(tx);
};
@ -602,13 +550,22 @@ TXDB.prototype.resolveOrphans = co(function* resolveOrphans(tx, index) {
* @returns {Promise}
*/
TXDB.prototype.add = co(function* add(tx, info) {
var unlock = yield this.locker.lock();
TXDB.prototype.add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
var result;
this.wallet.start();
try {
return yield this._add(tx, info);
} finally {
unlock();
result = yield this._add(tx, info);
} catch (e) {
this.wallet.drop();
throw e;
}
yield this.wallet.commit();
return result;
});
/**
@ -633,16 +590,9 @@ TXDB.prototype._add = co(function* add(tx, info) {
if (result)
return true;
this.start();
// Verify and get coins.
// This potentially removes double-spenders.
try {
result = yield this.verify(tx, info);
} catch (e) {
this.drop();
throw e;
}
result = yield this.verify(tx, info);
if (!result)
return false;
@ -692,12 +642,7 @@ TXDB.prototype._add = co(function* add(tx, info) {
// Add orphan, if no parent transaction is yet known
if (!input.coin) {
try {
yield this.addOrphan(prevout, spender);
} catch (e) {
this.drop();
throw e;
}
yield this.addOrphan(prevout, spender);
continue;
}
@ -722,12 +667,7 @@ TXDB.prototype._add = co(function* add(tx, info) {
if (!path)
continue;
try {
orphans = yield this.resolveOrphans(tx, i);
} catch (e) {
this.drop();
throw e;
}
orphans = yield this.resolveOrphans(tx, i);
if (orphans)
continue;
@ -744,8 +684,6 @@ TXDB.prototype._add = co(function* add(tx, info) {
this.coinCache.set(key, coin);
}
yield this.commit();
// Clear any locked coins to free up memory.
this.unlockTX(tx);
@ -925,8 +863,6 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
// Save the original received time.
tx.ps = existing.ps;
this.start();
this.put(layout.t(hash), tx.toExtended());
this.del(layout.p(hash));
@ -947,21 +883,11 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
if (!info.hasPath(address))
continue;
try {
coin = yield this.getCoin(hash, i);
} catch (e) {
this.drop();
throw e;
}
coin = yield this.getCoin(hash, i);
// Update spent coin.
if (!coin) {
try {
yield this.updateSpentCoin(tx, i);
} catch (e) {
this.drop();
throw e;
}
yield this.updateSpentCoin(tx, i);
continue;
}
@ -975,8 +901,6 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
this.coinCache.set(key, coin);
}
yield this.commit();
this.emit('tx', tx, info);
this.emit('confirmed', tx, info);
@ -990,12 +914,20 @@ TXDB.prototype.confirm = co(function* confirm(tx, info) {
*/
TXDB.prototype.remove = co(function* remove(hash) {
var unlock = yield this.locker.lock();
var result;
this.wallet.start();
try {
return yield this._remove(hash);
} finally {
unlock();
result = yield this._remove(hash);
} catch (e) {
this.wallet.drop();
throw e;
}
yield this.wallet.commit();
return result;
});
/**
@ -1012,16 +944,7 @@ TXDB.prototype._remove = co(function* remove(hash) {
if (!tx)
return;
this.start();
try {
info = yield this.removeRecursive(tx);
} catch (e) {
this.drop();
throw e;
}
yield this.commit();
info = yield this.removeRecursive(tx);
if (!info)
return;
@ -1038,7 +961,7 @@ TXDB.prototype._remove = co(function* remove(hash) {
*/
TXDB.prototype.lazyRemove = co(function* lazyRemove(tx) {
var info = yield this.getInfo(tx);
var info = yield this.getPathInfo(tx);
if (!info)
return;
@ -1143,12 +1066,20 @@ TXDB.prototype.__remove = co(function* remove(tx, info) {
*/
TXDB.prototype.unconfirm = co(function* unconfirm(hash) {
var unlock = yield this.locker.lock();
var result;
this.wallet.start();
try {
return yield this._unconfirm(hash);
} finally {
unlock();
result = yield this._unconfirm(hash);
} catch (e) {
this.wallet.drop();
throw e;
}
yield this.wallet.commit();
return result;
});
/**
@ -1165,21 +1096,12 @@ TXDB.prototype._unconfirm = co(function* unconfirm(hash) {
if (!tx)
return false;
info = yield this.getInfo(tx);
info = yield this.getPathInfo(tx);
if (!info)
return false;
this.start();
try {
result = yield this.__unconfirm(tx, info);
} catch (e) {
this.drop();
throw e;
}
yield this.commit();
result = yield this.__unconfirm(tx, info);
return result;
});
@ -1804,7 +1726,7 @@ TXDB.prototype.toDetails = co(function* toDetails(tx) {
yield this.fillHistory(tx);
info = yield this.getInfo(tx);
info = yield this.getPathInfo(tx);
if (!info)
throw new Error('Info not found.');
@ -1996,23 +1918,6 @@ TXDB.prototype.getAccountBalance = co(function* getBalance(account) {
*/
TXDB.prototype.zap = co(function* zap(account, age) {
var unlock = yield this.locker.lock();
try {
return yield this._zap(account, age);
} finally {
unlock();
}
});
/**
* Zap pending transactions without a lock.
* @private
* @param {Number?} account
* @param {Number} age
* @returns {Promise}
*/
TXDB.prototype._zap = co(function* zap(account, age) {
var i, txs, tx, hash;
if (!utils.isUInt32(age))
@ -2030,7 +1935,16 @@ TXDB.prototype._zap = co(function* zap(account, age) {
if (tx.ts !== 0)
continue;
yield this._remove(hash);
this.wallet.start();
try {
yield this._remove(hash);
} catch (e) {
this.wallet.drop();
throw e;
}
yield this.wallet.commit();
}
});

View File

@ -535,16 +535,42 @@ Wallet.prototype.renameAccount = co(function* renameAccount(acct, name) {
*/
Wallet.prototype._renameAccount = co(function* _renameAccount(acct, name) {
var account;
var i, account, old, paths, path;
assert(utils.isName(name), 'Bad account name.');
if (!utils.isName(name))
throw new Error('Bad account name.');
account = yield this.getAccount(acct);
if (!account)
throw new Error('Account not found.');
yield this.db.renameAccount(account, name);
if (account.accountIndex === 0)
throw new Error('Cannot rename default account.');
if (yield this.hasAccount(name))
throw new Error('Account name not available.');
old = account.name;
this.start();
this.db.renameAccount(account, name);
yield this.commit();
this.indexCache.remove(old);
paths = this.pathCache.values();
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.account !== account.accountIndex)
continue;
path.name = name;
}
});
/**
@ -766,7 +792,7 @@ Wallet.prototype.getAddressHashes = function getAddressHashes() {
*/
Wallet.prototype.getAccount = co(function* getAccount(acct) {
var index, unlock, account;
var index, unlock;
if (this.account) {
if (acct === 0 || acct === 'default')
@ -805,13 +831,12 @@ Wallet.prototype._getAccount = co(function* getAccount(index) {
return;
account.wallet = this;
yield account.open();
account.wid = this.wid;
account.id = this.id;
account.watchOnly = this.watchOnly;
yield account.open();
this.accountCache.set(index, account);
return account;
@ -825,7 +850,7 @@ Wallet.prototype._getAccount = co(function* getAccount(index) {
*/
Wallet.prototype.getAccountIndex = co(function* getAccountIndex(name) {
var key, index;
var index;
if (name == null)
return -1;
@ -1070,6 +1095,7 @@ Wallet.prototype.getPaths = co(function* getPaths(acct) {
if (!account || path.account === account) {
path.id = this.id;
path.name = yield this.getAccountName(path.account);
this.pathCache.set(path.hash, path);
out.push(path);
}
}
@ -1568,29 +1594,11 @@ Wallet.prototype.getOutputPaths = co(function* getOutputPaths(tx) {
*/
Wallet.prototype.syncOutputDepth = co(function* syncOutputDepth(info) {
var unlock = yield this.writeLock.lock();
try {
return yield this._syncOutputDepth(info);
} finally {
unlock();
}
});
/**
* Sync address depths without a lock.
* @private
* @param {PathInfo} info
* @returns {Promise} - Returns Boolean
*/
Wallet.prototype._syncOutputDepth = co(function* syncOutputDepth(info) {
var derived = [];
var accounts = {};
var i, j, path, paths, account;
var receive, change, nested, ring;
this.start();
for (i = 0; i < info.paths.length; i++) {
path = info.paths[i];
@ -1646,8 +1654,6 @@ Wallet.prototype._syncOutputDepth = co(function* syncOutputDepth(info) {
derived.push(ring);
}
yield this.commit();
if (derived.length > 0) {
this.db.emit('address', this.id, derived);
this.emit('address', derived);
@ -1678,19 +1684,6 @@ Wallet.prototype.updateBalances = co(function* updateBalances() {
this.emit('balance', balance);
});
/**
* Derive new addresses and emit balance.
* @private
* @param {TX} tx
* @param {PathInfo} info
* @returns {Promise}
*/
Wallet.prototype.handleTX = co(function* handleTX(info) {
yield this.syncOutputDepth(info);
yield this.updateBalances();
});
/**
* Get a redeem script or witness script by hash.
* @param {Hash} hash - Can be a ripemd160 or a sha256.
@ -1820,20 +1813,123 @@ Wallet.prototype.getTX = function getTX(hash) {
* @returns {Promise}
*/
Wallet.prototype.addTX = function addTX(tx) {
return this.db.addTX(tx);
};
Wallet.prototype.add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
yield this.txdb.add(tx, info);
yield this.handleTX(info);
var unlock = yield this.writeLock.lock();
try {
return yield this._add(tx);
} finally {
unlock();
}
});
/**
* Add a transaction to the wallet without a lock.
* @param {TX} tx
* @returns {Promise}
*/
Wallet.prototype._add = co(function* add(tx) {
var info = yield this.getPathInfo(tx);
this.start();
try {
yield this.txdb._add(tx, info);
yield this.syncOutputDepth(info);
yield this.updateBalances();
} catch (e) {
this.drop();
throw e;
}
yield this.commit();
});
/**
* Unconfirm a wallet transcation.
* @param {Hash} hash
* @returns {Promise}
*/
Wallet.prototype.unconfirm = co(function* unconfirm(hash) {
return yield this.txdb.unconfirm(hash);
var unlock = yield this.writeLock.lock();
try {
return yield this.txdb.unconfirm(hash);
} finally {
unlock();
}
});
/**
* Remove a wallet transaction.
* @param {Hash} hash
* @returns {Promise}
*/
Wallet.prototype.remove = co(function* remove(hash) {
var unlock = yield this.writeLock.lock();
try {
return yield this.txdb.remove(hash);
} finally {
unlock();
}
});
/**
* Zap stale TXs from wallet (accesses db).
* @param {(Number|String)?} acct
* @param {Number} age - Age threshold (unix time, default=72 hours).
* @returns {Promise}
*/
Wallet.prototype.zap = co(function* zap(acct, age) {
var unlock = yield this.writeLock.lock();
try {
return yield this._zap(acct, age);
} finally {
unlock();
}
});
/**
* Zap stale TXs from wallet without a lock.
* @private
* @param {(Number|String)?} acct
* @param {Number} age
* @returns {Promise}
*/
Wallet.prototype._zap = co(function* zap(acct, age) {
var account = yield this._getIndex(acct);
return yield this.txdb.zap(account, age);
});
/**
* Abandon transaction (accesses db).
* @param {Hash} hash
* @returns {Promise}
*/
Wallet.prototype.abandon = co(function* abandon(hash) {
var unlock = yield this.writeLock.lock();
try {
return yield this._abandon(hash);
} finally {
unlock();
}
});
/**
* Abandon transaction without a lock.
* @private
* @param {Hash} hash
* @returns {Promise}
*/
Wallet.prototype._abandon = function abandon(hash) {
return this.txdb.abandon(hash);
};
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
@ -1930,28 +2026,6 @@ Wallet.prototype.getLast = co(function* getLast(acct, limit) {
return yield this.txdb.getLast(account, limit);
});
/**
* Zap stale TXs from wallet (accesses db).
* @param {(Number|String)?} acct
* @param {Number} age - Age threshold (unix time, default=72 hours).
* @returns {Promise}
*/
Wallet.prototype.zap = co(function* zap(acct, age) {
var account = yield this._getIndex(acct);
return yield this.txdb.zap(account, age);
});
/**
* Abandon transaction (accesses db).
* @param {Hash} hash
* @returns {Promise}
*/
Wallet.prototype.abandon = function abandon(hash) {
return this.txdb.abandon(hash);
};
/**
* Resolve account index.
* @private

View File

@ -25,7 +25,6 @@ var ldb = require('../db/ldb');
var Bloom = require('../utils/bloom');
var Logger = require('../node/logger');
var TX = require('../primitives/tx');
var PathInfo = require('./pathinfo');
/*
* Database Layout:
@ -534,79 +533,44 @@ WalletDB.prototype._rename = co(function* _rename(wallet, id) {
var old = wallet.id;
var i, paths, path, batch;
assert(utils.isName(id), 'Bad wallet ID.');
if (!utils.isName(id))
throw new Error('Bad wallet ID.');
if (yield this.has(id))
throw new Error('ID not available.');
batch = this.start(wallet);
batch.del(layout.l(old));
wallet.id = id;
this.save(wallet);
yield this.commit(wallet);
this.widCache.remove(old);
paths = wallet.pathCache.values();
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.wid !== wallet.wid)
continue;
path.id = id;
}
wallet.id = id;
batch = this.start(wallet);
batch.del(layout.l(old));
this.save(wallet);
yield this.commit(wallet);
});
/**
* Rename an account.
* @param {Account} account
* @param {String} name
* @returns {Promise}
*/
WalletDB.prototype.renameAccount = co(function* renameAccount(account, name) {
WalletDB.prototype.renameAccount = function renameAccount(account, name) {
var wallet = account.wallet;
var old = account.name;
var i, paths, path, batch;
assert(utils.isName(name), 'Bad account name.');
if (account.accountIndex === 0)
throw new Error('Cannot rename primary account.');
if (yield account.wallet.hasAccount(name))
throw new Error('Account name not available.');
wallet.indexCache.remove(old);
paths = wallet.pathCache.values();
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.wid !== account.wid)
continue;
if (path.account !== account.accountIndex)
continue;
path.name = name;
}
var batch = this.batch(wallet);
batch.del(layout.i(account.wid, account.name));
account.name = name;
batch = this.start(wallet);
batch.del(layout.i(account.wid, old));
this.saveAccount(account);
yield this.commit(wallet);
});
};
/**
* Test an api key against a wallet's api key.
@ -669,10 +633,10 @@ WalletDB.prototype._create = co(function* create(options) {
wallet = Wallet.fromOptions(this, options);
wallet.wid = this.depth++;
this.register(wallet);
yield wallet.init(options);
this.register(wallet);
this.logger.info('Created wallet %s.', wallet.id);
return wallet;
@ -712,7 +676,6 @@ WalletDB.prototype.ensure = co(function* ensure(options) {
WalletDB.prototype.getAccount = co(function* getAccount(wid, index) {
var data = yield this.db.get(layout.a(wid, index));
var account;
if (!data)
return;
@ -919,6 +882,7 @@ WalletDB.prototype.getPaths = co(function* getPaths(hash) {
WalletDB.prototype.getPath = co(function* getPath(wid, hash) {
var data = yield this.db.get(layout.P(wid, hash));
var path;
if (!data)
return;
@ -1170,7 +1134,7 @@ WalletDB.prototype.resend = co(function* resend() {
WalletDB.prototype.getWidsByHashes = co(function* getWidsByHashes(hashes) {
var result = [];
var i, j, hash, wids, wid;
var i, j, hash, wids;
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
@ -1350,7 +1314,7 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) {
for (i = 0; i < txs.length; i++) {
tx = txs[i];
wallets = yield this._add(tx);
wallets = yield this._addTX(tx);
if (!wallets)
continue;
@ -1393,7 +1357,7 @@ WalletDB.prototype.removeBlock = co(function* removeBlock(entry) {
WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
var block = WalletBlock.fromEntry(entry);
var i, j, data, hash, wallets, wid, wallet;
var i, data, hash;
// If we crash during a reorg, there's not much to do.
// Reorgs cannot be rescanned. The database will be
@ -1416,7 +1380,7 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
for (i = 0; i < block.hashes.length; i++) {
hash = block.hashes[i];
yield this._unconfirm(hash);
yield this._unconfirmTX(hash);
}
this.tip = block.hash;
@ -1431,11 +1395,10 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
* @returns {Promise}
*/
WalletDB.prototype.addTX =
WalletDB.prototype.add = co(function* add(tx) {
WalletDB.prototype.addTX = co(function* addTX(tx) {
var unlock = yield this.txLock.lock();
try {
return yield this._add(tx);
return yield this._addTX(tx);
} finally {
unlock();
}
@ -1448,7 +1411,7 @@ WalletDB.prototype.add = co(function* add(tx) {
* @returns {Promise}
*/
WalletDB.prototype._add = co(function* add(tx) {
WalletDB.prototype._addTX = co(function* addTX(tx) {
var i, hashes, wallets, wid, wallet;
assert(!tx.mutable, 'Cannot add mutable TX to wallet.');
@ -1482,30 +1445,29 @@ WalletDB.prototype._add = co(function* add(tx) {
});
/**
* Add a transaction to the database, map addresses
* to wallet IDs, potentially store orphans, resolve
* orphans, or confirm a transaction.
* @param {TX} tx
* Unconfirm a transaction from all relevant wallets.
* @param {Hash} hash
* @returns {Promise}
*/
WalletDB.prototype.unconfirm = co(function* unconfirm(hash) {
WalletDB.prototype.unconfirmTX = co(function* unconfirmTX(hash) {
var unlock = yield this.txLock.lock();
try {
return yield this._unconfirm(tx);
return yield this._unconfirmTX(hash);
} finally {
unlock();
}
});
/**
* Add a transaction to the database without a lock.
* Unconfirm a transaction from all
* relevant wallets without a lock.
* @private
* @param {TX} tx
* @param {Hash} hash
* @returns {Promise}
*/
WalletDB.prototype._unconfirm = co(function* unconfirm(hash) {
WalletDB.prototype._unconfirmTX = co(function* unconfirmTX(hash) {
var wallets = yield this.getWalletsByTX(hash);
var i, wid, wallet;