ensure full atomicity in wallet.

This commit is contained in:
Christopher Jeffrey 2016-07-13 20:50:31 -07:00
parent 850b16fa7d
commit 196f3ca861
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 247 additions and 113 deletions

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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);
};
/**