diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index ef4c449e..247e2740 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -96,7 +96,7 @@ TXDB.prototype._lock = function _lock(func, args, force) { * @param {Function} callback */ -TXDB.prototype._loadFilter = function loadFilter(callback) { +TXDB.prototype.loadFilter = function loadFilter(callback) { var self = this; if (!this.filter) @@ -119,7 +119,7 @@ TXDB.prototype._loadFilter = function loadFilter(callback) { * @returns {Boolean} */ -TXDB.prototype._testFilter = function _testFilter(addresses) { +TXDB.prototype.testFilter = function testFilter(addresses) { var i; if (!this.filter) @@ -143,7 +143,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) { var addresses = tx.getHashes('hex'); var map; - if (!this._testFilter(addresses)) + if (!this.testFilter(addresses)) return callback(); this.mapAddresses(addresses, function(err, table) { @@ -281,6 +281,34 @@ TXDB.prototype._getOrphans = function _getOrphans(key, callback) { }); }; +/** + * Write the genesis block as the best hash. + * @param {Function} callback + */ + +TXDB.prototype.writeGenesis = function writeGenesis(callback) { + var self = this; + var unlock, hash; + + unlock = this._lock(writeGenesis, [callback]); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + self.db.has('R', function(err, result) { + if (err) + return callback(err); + + if (result) + return callback(); + + hash = new Buffer(self.network.genesis.hash, 'hex'); + self.db.put('R', hash, callback); + }); +}; + /** * Add a block's transactions and write the new best hash. * @param {Block} block diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 65e7d22c..1ca85241 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -50,7 +50,8 @@ function Wallet(db, options) { this.db = db; this.network = db.network; - this.locker = new bcoin.locker(this); + this.writeLock = new bcoin.locker(this); + this.fillLock = new bcoin.locker(this); this.id = null; this.master = null; @@ -220,6 +221,7 @@ Wallet.prototype.destroy = function destroy(callback) { */ Wallet.prototype.addKey = function addKey(account, key, callback) { + var self = this; var unlock; if (typeof key === 'function') { @@ -228,7 +230,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) { account = 0; } - unlock = this.locker.lock(addKey, [account, key, callback]); + unlock = this.writeLock.lock(addKey, [account, key, callback]); if (!unlock) return; @@ -242,7 +244,19 @@ Wallet.prototype.addKey = function addKey(account, key, callback) { if (!account) return callback(new Error('Account not found.')); - account.addKey(key, callback); + self.start(); + + account.addKey(key, function(err, result) { + if (err) { + self.drop(); + return callback(err); + } + self.commit(function(err) { + if (err) + return callback(err); + return callback(null, result); + }); + }); }, true); }; @@ -254,6 +268,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) { */ Wallet.prototype.removeKey = function removeKey(account, key, callback) { + var self = this; var unlock; if (typeof key === 'function') { @@ -262,7 +277,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) { account = 0; } - unlock = this.locker.lock(removeKey, [account, key, callback]); + unlock = this.writeLock.lock(removeKey, [account, key, callback]); if (!unlock) return; @@ -276,7 +291,19 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) { if (!account) return callback(new Error('Account not found.')); - account.removeKey(key, callback); + self.start(); + + account.removeKey(key, function(err, result) { + if (err) { + self.drop(); + return callback(err); + } + self.commit(function(err) { + if (err) + return callback(err); + return callback(null, result); + }); + }); }, true); }; @@ -297,7 +324,7 @@ Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) { old = null; } - unlock = this.locker.lock(setPassphrase, [old, new_, callback]); + unlock = this.writeLock.lock(setPassphrase, [old, new_, callback]); if (!unlock) return; @@ -312,7 +339,9 @@ Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) { if (err) return callback(err); - return self.save(callback); + self.start(); + self.save(); + self.commit(callback); }); }); }; @@ -332,7 +361,7 @@ Wallet.prototype.retoken = function retoken(passphrase, callback) { passphrase = null; } - unlock = this.locker.lock(retoken, [passphrase, callback]); + unlock = this.writeLock.lock(retoken, [passphrase, callback]); if (!unlock) return; @@ -346,7 +375,9 @@ Wallet.prototype.retoken = function retoken(passphrase, callback) { self.tokenDepth++; self.token = self.getToken(master, self.tokenDepth); - self.save(function(err) { + self.start(); + self.save(); + self.commit(function(err) { if (err) return callback(err); return callback(null, self.token); @@ -431,7 +462,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force var self = this; var key, unlock; - unlock = this.locker.lock(createAccount, [options, callback], force); + unlock = this.writeLock.lock(createAccount, [options, callback], force); if (!unlock) return; @@ -457,13 +488,17 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force n: options.n }; + self.start(); + self.db.createAccount(options, function(err, account) { - if (err) + if (err) { + self.drop(); return callback(err); + } self.accountDepth++; - - self.save(function(err) { + self.save(); + self.commit(function(err) { if (err) return callback(err); return callback(null, account); @@ -487,14 +522,7 @@ Wallet.prototype.getAccounts = function getAccounts(callback) { * @param {Function} callback - Returns [Error, {@link Account}]. */ -Wallet.prototype.getAccount = function getAccount(account, callback, force) { - var unlock = this.locker.lock(getAccount, [account, callback], force); - - if (!unlock) - return; - - callback = utils.wrap(callback, unlock); - +Wallet.prototype.getAccount = function getAccount(account, callback) { if (this.account) { if (account === 0 || account === 'default') return callback(null, this.account); @@ -547,7 +575,7 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac account = 0; } - unlock = this.locker.lock(createAddress, [account, change, callback]); + unlock = this.writeLock.lock(createAddress, [account, change, callback]); if (!unlock) return; @@ -561,7 +589,19 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac if (!account) return callback(new Error('Account not found.')); - account.createAddress(change, callback); + self.start(); + + account.createAddress(change, function(err, result) { + if (err) { + self.drop(); + return callback(err); + } + self.commit(function(err) { + if (err) + return callback(err); + return callback(null, result); + }); + }); }, true); }; @@ -571,8 +611,35 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac * @param {Function} callback */ -Wallet.prototype.save = function save(callback) { - return this.db.save(this, callback); +Wallet.prototype.save = function save() { + return this.db.save(this); +}; + +/** + * Start batch. + * @private + */ + +Wallet.prototype.start = function start() { + return this.db.start(this.id); +}; + +/** + * Drop batch. + * @private + */ + +Wallet.prototype.drop = function drop() { + return this.db.drop(this.id); +}; + +/** + * Save batch. + * @param {Function} callback + */ + +Wallet.prototype.commit = function commit(callback) { + return this.db.commit(this.id, callback); }; /** @@ -620,7 +687,7 @@ Wallet.prototype.getPath = function getPath(address, callback) { Wallet.prototype.fill = function fill(tx, options, callback) { var self = this; - var rate; + var unlock, rate; if (typeof options === 'function') { callback = options; @@ -630,6 +697,15 @@ Wallet.prototype.fill = function fill(tx, options, callback) { if (!options) options = {}; + // We use a lock here to ensure we + // don't end up double spending coins. + unlock = this.fillLock.lock(fill, [tx, options, callback]); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + if (!this.initialized) return callback(new Error('Wallet is not initialized.')); @@ -920,7 +996,7 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { var receive = []; var i, path, unlock; - unlock = this.locker.lock(syncOutputDepth, [tx, callback]); + unlock = this.writeLock.lock(syncOutputDepth, [tx, callback]); if (!unlock) return; @@ -940,6 +1016,8 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { accounts[path.account].push(path); } + self.start(); + utils.forEachSerial(Object.keys(accounts), function(index, next) { var paths = accounts[index]; var receiveDepth = -1; @@ -979,12 +1057,18 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) { next(); }); - }, true); + }); }, function(err) { - if (err) + if (err) { + self.drop(); return callback(err); + } - return callback(null, receive, change); + self.commit(function(err) { + if (err) + return callback(err); + return callback(null, receive, change); + }); }); }); }; @@ -1049,7 +1133,7 @@ Wallet.prototype.scan = function scan(maxGap, scanner, callback) { maxGap = null; } - unlock = this.locker.lock(scan, [maxGap, scanner, callback]); + unlock = this.writeLock.lock(scan, [maxGap, scanner, callback]); if (!unlock) return; @@ -1059,17 +1143,31 @@ Wallet.prototype.scan = function scan(maxGap, scanner, callback) { if (!this.initialized) return callback(new Error('Wallet is not initialized.')); + self.start(); + + function done(err, total) { + if (err) { + self.drop(); + return callback(err); + } + self.commit(function(err) { + if (err) + return callback(err); + return callback(null, total); + }); + } + (function next() { self.getAccount(index++, function(err, account) { if (err) - return callback(err); + return done(err); if (!account) - return callback(null, total); + return done(null, total); account.scan(maxGap, scanner, function(err, result) { if (err) - return callback(err); + return done(err); total += result; @@ -1829,7 +1927,8 @@ Account.prototype.init = function init(callback) { // Waiting for more keys. if (this.keys.length !== this.n) { assert(!this.initialized); - return this.save(callback); + this.save(); + return callback(); } assert(this.receiveDepth === 0); @@ -1870,14 +1969,6 @@ Account.prototype.pushKey = function pushKey(key) { assert(key, 'Key required.'); - if (Array.isArray(key)) { - for (i = 0; i < key.length; i++) { - if (this.pushKey(key[i])) - result = true; - } - return result; - } - if (key.accountKey) key = key.accountKey; @@ -1924,14 +2015,6 @@ Account.prototype.spliceKey = function spliceKey(key) { var index = -1; var i; - if (Array.isArray(key)) { - for (i = 0; i < key.length; i++) { - if (this.spliceKey(key[i])) - result = true; - } - return result; - } - assert(key, 'Key required.'); if (key.accountKey) @@ -1976,12 +2059,11 @@ Account.prototype.spliceKey = function spliceKey(key) { Account.prototype.addKey = function addKey(key, callback) { var result = false; - var error; try { result = this.pushKey(key); } catch (e) { - error = e; + return callback(e); } // Try to initialize again. @@ -1989,9 +2071,6 @@ Account.prototype.addKey = function addKey(key, callback) { if (err) return callback(err); - if (error) - return callback(error); - return callback(null, result); }); }; @@ -2005,23 +2084,16 @@ Account.prototype.addKey = function addKey(key, callback) { Account.prototype.removeKey = function removeKey(key, callback) { var result = false; - var error; try { result = this.spliceKey(key); } catch (e) { - error = e; + return callback(e); } - this.save(function(err) { - if (err) - return callback(err); + this.save(); - if (error) - return callback(error); - - return callback(null, result); - }); + return callback(null, result); }; /** @@ -2076,11 +2148,9 @@ Account.prototype.createAddress = function createAddress(change, callback) { if (err) return callback(err); - self.save(function(err) { - if (err) - return callback(err); - return callback(null, address); - }); + self.save(); + + return callback(null, address); }); }; @@ -2149,8 +2219,8 @@ Account.prototype.deriveAddress = function deriveAddress(change, index) { * @param {Function} callback */ -Account.prototype.save = function save(callback) { - return this.db.saveAccount(this, callback); +Account.prototype.save = function save() { + return this.db.saveAccount(this); }; /** @@ -2209,12 +2279,9 @@ Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callba if (err) return callback(err); - self.save(function(err) { - if (err) - return callback(err); + self.save(); - return callback(null, receive, change); - }); + return callback(null, receive, change); }); }; @@ -2284,6 +2351,7 @@ Account.prototype.scan = function scan(maxGap, scanner, callback) { if (maxGap === 0 && index === depth) { if (!change) return chainCheck(true); + self.save(); return callback(null, total); } @@ -2305,11 +2373,7 @@ Account.prototype.scan = function scan(maxGap, scanner, callback) { self.changeDepth = Math.max(depth, self.changeDepth - gap); self.changeAddress = self.deriveChange(self.changeDepth - 1); - self.save(function(err) { - if (err) - return callback(err); - return callback(null, total); - }); + return callback(null, total); }); }); }); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 4998e52b..1231ebfd 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -50,13 +50,14 @@ function WalletDB(options) { this.network = bcoin.network.get(options.network); this.fees = options.fees; this.logger = options.logger || bcoin.defaultLogger; + this.batches = {}; // We need one read lock for `get` and `create`. // It will hold locks specific to wallet ids. this.readLock = new ReadLock(this); - this.accountCache = new bcoin.lru(10000, 1); this.walletCache = new bcoin.lru(10000, 1); + this.accountCache = new bcoin.lru(10000, 1); this.pathCache = new bcoin.lru(100000, 1); this.db = bcoin.ldb({ @@ -139,7 +140,12 @@ WalletDB.prototype._open = function open(callback) { if (err) return callback(err); - self.tx._loadFilter(callback); + self.tx.writeGenesis(function(err) { + if (err) + return callback(err); + + self.tx.loadFilter(callback); + }); }); }); }; @@ -176,6 +182,58 @@ WalletDB.prototype._lock = function lock(id, func, args, force) { return this.readLock.lock(id, func, args, force); }; +/** + * Start batch. + * @private + * @param {WalletID} id + */ + +WalletDB.prototype.start = function start(id) { + assert(utils.isAlpha(id), 'Bad ID for batch.'); + assert(!this.batches[id], 'Batch already started.'); + this.batches[id] = this.db.batch(); +}; + +/** + * Drop batch. + * @private + * @param {WalletID} id + */ + +WalletDB.prototype.drop = function drop(id) { + var batch = this.batch(id); + batch.clear(); + delete this.batches[id]; +}; + +/** + * Get batch. + * @private + * @param {WalletID} id + * @returns {Leveldown.Batch} + */ + +WalletDB.prototype.batch = function batch(id) { + var batch; + assert(utils.isAlpha(id), 'Bad ID for batch.'); + batch = this.batches[id]; + assert(batch, 'Batch does not exist.'); + return batch; +}; + +/** + * Save batch. + * @private + * @param {WalletID} id + * @param {Function} callback + */ + +WalletDB.prototype.commit = function commit(id, callback) { + var batch = this.batch(id); + delete this.batches[id]; + batch.write(callback); +}; + /** * Emit balance events after a tx is saved. * @private @@ -495,13 +553,10 @@ WalletDB.prototype._get = function get(id, callback) { * @param {Function} callback */ -WalletDB.prototype.save = function save(wallet, callback) { - if (!utils.isAlpha(wallet.id)) - return callback(new Error('Wallet IDs must be alphanumeric.')); - +WalletDB.prototype.save = function save(wallet) { + var batch = this.batch(wallet.id); this.walletCache.set(wallet.id, wallet); - - this.db.put('w/' + wallet.id, wallet.toRaw(), callback); + batch.put('w/' + wallet.id, wallet.toRaw()); }; /** @@ -770,25 +825,17 @@ WalletDB.prototype.getAccountIndex = function getAccountIndex(id, name, callback * @param {Function} callback */ -WalletDB.prototype.saveAccount = function saveAccount(account, callback) { - var index, key, batch; +WalletDB.prototype.saveAccount = function saveAccount(account) { + var batch = this.batch(account.id); + var index = new Buffer(4); + var key = account.id + '/' + account.accountIndex; - if (!utils.isAlpha(account.name)) - return callback(new Error('Account names must be alphanumeric.')); - - batch = this.db.batch(); - - index = new Buffer(4); index.writeUInt32LE(account.accountIndex, 0, true); - key = account.id + '/' + account.accountIndex; - batch.put('a/' + key, account.toRaw()); batch.put('i/' + account.id + '/' + account.name, index); this.accountCache.set(key, account); - - batch.write(callback); }; /** @@ -863,7 +910,7 @@ WalletDB.prototype.hasAccount = function hasAccount(id, account, callback) { WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) { var self = this; var items = []; - var batch = this.db.batch(); + var batch = this.batch(id); var i, address, path; if (!Array.isArray(addresses)) @@ -910,12 +957,7 @@ WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) { next(); }); - }, function(err) { - if (err) - return callback(err); - - batch.write(callback); - }); + }, callback); }; /**