From 08c7136ef430eb0b05d4eda10409b5492d37c00d Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 4 Oct 2016 00:33:07 -0700 Subject: [PATCH] wallet: share batches between wallet and txdb. --- lib/wallet/pathinfo.js | 25 +++-- lib/wallet/txdb.js | 212 ++++++++++++---------------------------- lib/wallet/wallet.js | 216 +++++++++++++++++++++++++++-------------- lib/wallet/walletdb.js | 104 +++++++------------- 4 files changed, 260 insertions(+), 297 deletions(-) diff --git a/lib/wallet/pathinfo.js b/lib/wallet/pathinfo.js index 10374765..444c5dde 100644 --- a/lib/wallet/pathinfo.js +++ b/lib/wallet/pathinfo.js @@ -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; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 0e06a6e8..8644db04 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -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(); } }); diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index ce88a932..f6837690 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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 diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 90f25fe4..4e8e3ec0 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -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;