walletdb: path refactor.

This commit is contained in:
Christopher Jeffrey 2016-09-30 23:46:13 -07:00
parent 77e1af4faf
commit 2544e5310a
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 307 additions and 93 deletions

View File

@ -13,6 +13,9 @@ var co = spawn.co;
var assert = utils.assert;
var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var Path = require('./path');
var Script = require('../script/script');
var KeyRing = require('../primitives/keyring');
/**
* Represents a BIP44 Account belonging to a {@link Wallet}.
@ -449,30 +452,31 @@ Account.prototype.deriveChange = function deriveChange(index, master) {
*/
Account.prototype.derivePath = function derivePath(path, master) {
var ring, raw;
var data = path.data;
var ring;
// Imported key.
if (path.index === -1) {
assert(path.imported);
assert(this.type === Account.types.PUBKEYHASH);
switch (path.keyType) {
case Path.types.HD:
return this.deriveAddress(path.change, path.index, master);
case Path.types.KEY:
assert(this.type === Account.types.PUBKEYHASH);
raw = path.imported;
if (path.encrypted) {
data = master.decipher(data, path.hash);
if (!data)
return;
}
if (path.encrypted)
raw = master.decipher(raw, path.hash);
ring = KeyRing.fromRaw(data, this.network);
ring.witness = this.witness;
ring.path = path;
if (!raw)
return ring;
case Path.types.ADDRESS:
return;
ring = bcoin.keyring.fromRaw(raw, this.network);
ring.path = path;
return ring;
default:
assert(false, 'Bad key type.');
}
ring = this.deriveAddress(path.change, path.index, master);
return ring;
};
/**
@ -495,7 +499,7 @@ Account.prototype.deriveAddress = function deriveAddress(change, index, master)
key = this.accountKey.derive(change).derive(index);
}
ring = bcoin.keyring.fromPublic(key.publicKey, this.network);
ring = KeyRing.fromPublic(key.publicKey, this.network);
ring.witness = this.witness;
switch (this.type) {
@ -510,7 +514,7 @@ Account.prototype.deriveAddress = function deriveAddress(change, index, master)
keys.push(shared.publicKey);
}
ring.script = bcoin.script.fromMultisig(this.m, this.n, keys);
ring.script = Script.fromMultisig(this.m, this.n, keys);
break;
}
@ -518,7 +522,7 @@ Account.prototype.deriveAddress = function deriveAddress(change, index, master)
if (key.privateKey)
ring.privateKey = key.privateKey;
ring.path = bcoin.path.fromAccount(this, ring, change, index);
ring.path = Path.fromHD(this, ring, change, index);
return ring;
};
@ -543,6 +547,16 @@ Account.prototype.saveAddress = function saveAddress(rings) {
return this.db.saveAddress(this.wid, rings);
};
/**
* Save paths to path map.
* @param {Path[]} rings
* @returns {Promise}
*/
Account.prototype.savePath = function savePath(paths) {
return this.db.savePath(this.wid, paths);
};
/**
* Set change and receiving depth (depth is the index of the _next_ address).
* Allocate all addresses up to depth. Note that this also allocates

View File

@ -12,6 +12,7 @@ var assert = utils.assert;
var constants = bcoin.constants;
var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var Address = require('../primitives/address');
/**
* Path
@ -34,9 +35,10 @@ function Path() {
this.account = 0;
this.change = -1;
this.index = -1;
this.keyType = -1;
this.encrypted = false;
this.imported = null;
this.data = null;
// Currently unused.
this.type = bcoin.script.types.PUBKEYHASH;
@ -47,6 +49,18 @@ function Path() {
this.hash = null;
}
/**
* Path types.
* @enum {Number}
* @default
*/
Path.types = {
HD: 0,
KEY: 1,
ADDRESS: 2
};
/**
* Clone the path object.
* @returns {Path}
@ -60,9 +74,10 @@ Path.prototype.clone = function clone() {
path.account = this.account;
path.change = this.change;
path.index = this.index;
path.keyType = this.keyType;
path.encrypted = this.encrypted;
path.imported = this.imported;
path.data = this.data;
path.type = this.type;
path.version = this.version;
@ -84,17 +99,21 @@ Path.prototype.fromRaw = function fromRaw(data) {
this.wid = p.readU32();
this.account = p.readU32();
this.change = -1;
this.index = -1;
this.keyType = p.readU8();
switch (p.readU8()) {
case 0:
switch (this.keyType) {
case Path.types.HD:
this.change = p.readU32();
this.index = p.readU32();
break;
case 1:
case Path.types.KEY:
this.encrypted = p.readU8() === 1;
this.imported = p.readVarBytes();
this.change = -1;
this.index = -1;
this.data = p.readVarBytes();
break;
case Path.types.ADDRESS:
// Hash will be passed in by caller.
break;
default:
assert(false);
@ -128,16 +147,28 @@ Path.prototype.toRaw = function toRaw(writer) {
p.writeU32(this.wid);
p.writeU32(this.account);
if (this.index !== -1) {
assert(!this.imported);
p.writeU8(0);
p.writeU32(this.change);
p.writeU32(this.index);
} else {
assert(this.imported);
p.writeU8(1);
p.writeU8(this.encrypted ? 1 : 0);
p.writeVarBytes(this.imported);
p.writeU8(this.keyType);
switch (this.keyType) {
case Path.types.HD:
assert(!this.data);
assert(this.index !== -1);
p.writeU32(this.change);
p.writeU32(this.index);
break;
case Path.types.KEY:
assert(this.data);
assert(this.index === -1);
p.writeU8(this.encrypted ? 1 : 0);
p.writeVarBytes(this.data);
break;
case Path.types.ADDRESS:
assert(!this.data);
assert(this.index === -1);
break;
default:
assert(false);
break;
}
p.write8(this.version);
@ -150,22 +181,22 @@ Path.prototype.toRaw = function toRaw(writer) {
};
/**
* Inject properties from account.
* Inject properties from hd account.
* @private
* @param {WalletID} wid
* @param {Account} account
* @param {KeyRing} ring
* @param {Number} change
* @param {Number} index
*/
Path.prototype.fromAccount = function fromAccount(account, ring, change, index) {
Path.prototype.fromHD = function fromHD(account, ring, change, index) {
this.wid = account.wid;
this.name = account.name;
this.account = account.accountIndex;
this.change = change;
this.index = index;
if (change != null)
this.change = change;
if (index != null)
this.index = index;
this.keyType = Path.types.HD;
this.version = ring.witness ? 0 : -1;
this.type = ring.getType();
@ -176,15 +207,78 @@ Path.prototype.fromAccount = function fromAccount(account, ring, change, index)
return this;
};
/**
* Instantiate path from hd account and keyring.
* @param {Account} account
* @param {KeyRing} ring
* @param {Number} change
* @param {Number} index
* @returns {Path}
*/
Path.fromHD = function fromHD(account, ring, change, index) {
return new Path().fromHD(account, ring, change, index);
};
/**
* Inject properties from keyring.
* @private
* @param {Account} account
* @param {KeyRing} ring
*/
Path.prototype.fromKey = function fromKey(account, ring) {
this.wid = account.wid;
this.name = account.name;
this.account = account.accountIndex;
this.keyType = Path.types.KEY;
this.data = ring.toRaw();
this.version = ring.witness ? 0 : -1;
this.type = ring.getType();
this.id = account.id;
this.hash = ring.getHash('hex');
return this;
};
/**
* Instantiate path from keyring.
* @param {WalletID} wid
* @param {Account} account
* @param {KeyRing} ring
* @returns {Path}
*/
Path.fromAccount = function fromAccount(account, ring, change, index) {
return new Path().fromAccount(account, ring, change, index);
Path.fromKey = function fromKey(account, ring) {
return new Path().fromKey(account, ring);
};
/**
* Inject properties from address.
* @private
* @param {Account} account
* @param {Address} address
*/
Path.prototype.fromAddress = function fromAddress(account, address) {
this.wid = account.wid;
this.name = account.name;
this.account = account.accountIndex;
this.keyType = Path.types.ADDRESS;
this.version = address.version;
this.type = address.type;
this.id = account.id;
this.hash = address.getHash('hex');
return this;
};
/**
* Instantiate path from address.
* @param {Account} account
* @param {Address} address
* @returns {Path}
*/
Path.fromAddress = function fromAddress(account, address) {
return new Path().fromAddress(account, address);
};
/**
@ -193,6 +287,9 @@ Path.fromAccount = function fromAccount(account, ring, change, index) {
*/
Path.prototype.toPath = function toPath() {
if (this.keyType !== Path.types.HD)
return null;
return 'm/' + this.account
+ '\'/' + this.change
+ '/' + this.index;
@ -204,7 +301,7 @@ Path.prototype.toPath = function toPath() {
*/
Path.prototype.toAddress = function toAddress(network) {
return bcoin.address.fromHash(this.hash, this.type, this.version, network);
return Address.fromHash(this.hash, this.type, this.version, network);
};
/**
@ -220,39 +317,6 @@ Path.prototype.toJSON = function toJSON() {
};
};
/**
* Inject properties from json object.
* @private
* @param {Object} json
*/
Path.prototype.fromJSON = function fromJSON(json) {
var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX);
assert(indexes.length === 3);
assert(indexes[0] >= constants.hd.HARDENED);
indexes[0] -= constants.hd.HARDENED;
this.wid = json.wid;
this.id = json.id;
this.name = json.name;
this.account = indexes[0];
this.change = indexes[1];
this.index = indexes[2];
return this;
};
/**
* Instantiate path from json object.
* @param {Object} json
* @returns {Path}
*/
Path.fromJSON = function fromJSON(json) {
return new Path().fromJSON(json);
};
/**
* Inspect the path.
* @returns {String}

View File

@ -19,6 +19,7 @@ var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var TXDB = require('./txdb');
var Path = require('./path');
var Address = require('../primitives/address');
/**
* BIP44 Wallet
@ -739,7 +740,7 @@ Wallet.prototype.commit = function commit() {
*/
Wallet.prototype.hasAddress = function hasAddress(address) {
var hash = bcoin.address.getHash(address, 'hex');
var hash = Address.getHash(address, 'hex');
if (!hash)
return Promise.resolve(false);
return this.db.hasAddress(this.wid, hash);
@ -752,7 +753,7 @@ Wallet.prototype.hasAddress = function hasAddress(address) {
*/
Wallet.prototype.getPath = co(function* getPath(address) {
var hash = bcoin.address.getHash(address, 'hex');
var hash = Address.getHash(address, 'hex');
var path;
if (!hash)
@ -820,7 +821,7 @@ Wallet.prototype.importKey = co(function* importKey(account, ring, passphrase) {
*/
Wallet.prototype._importKey = co(function* importKey(account, ring, passphrase) {
var exists, raw, path;
var exists, path;
if (account && typeof account === 'object') {
passphrase = ring;
@ -846,16 +847,14 @@ Wallet.prototype._importKey = co(function* importKey(account, ring, passphrase)
yield this.unlock(passphrase);
raw = ring.toRaw();
path = Path.fromAccount(account, ring);
path = Path.fromKey(account, ring);
if (this.master.encrypted) {
raw = this.master.encipher(raw, path.hash);
assert(raw);
path.data = this.master.encipher(path.data, path.hash);
assert(path.data);
path.encrypted = true;
}
path.imported = raw;
ring.path = path;
this.start();
@ -870,6 +869,71 @@ Wallet.prototype._importKey = co(function* importKey(account, ring, passphrase)
yield this.commit();
});
/**
* Import a keyring (will not exist on derivation chain).
* Rescanning must be invoked manually.
* @param {(String|Number)?} account
* @param {KeyRing} ring
* @param {(String|Buffer)?} passphrase
* @returns {Promise}
*/
Wallet.prototype.importAddress = co(function* importAddress(account, address) {
var unlock = yield this.writeLock.lock();
try {
return yield this._importAddress(account, address);
} finally {
unlock();
}
});
/**
* Import a keyring (will not exist on derivation chain) without a lock.
* @private
* @param {(String|Number)?} account
* @param {KeyRing} ring
* @param {(String|Buffer)?} passphrase
* @returns {Promise}
*/
Wallet.prototype._importAddress = co(function* importAddress(account, address) {
var exists, path;
if (account instanceof Address) {
address = account;
account = null;
}
if (account == null)
account = 0;
exists = yield this.getPath(address.getHash('hex'));
if (exists)
throw new Error('Address already exists.');
account = yield this.getAccount(account);
if (!account)
throw new Error('Account not found.');
if (account.type !== bcoin.account.types.PUBKEYHASH)
throw new Error('Cannot import into non-pkh account.');
path = Path.fromAddress(account, address);
this.start();
try {
yield account.savePath([path], true);
} catch (e) {
this.drop();
throw e;
}
yield this.commit();
});
/**
* Fill a transaction with inputs, estimate
* transaction size, calculate fee, and add a change output.
@ -1111,7 +1175,7 @@ Wallet.prototype.deriveInputs = co(function* deriveInputs(tx) {
*/
Wallet.prototype.getKeyRing = co(function* getKeyRing(address) {
var hash = bcoin.address.getHash(address, 'hex');
var hash = Address.getHash(address, 'hex');
var path, account;
if (!hash)

View File

@ -18,6 +18,7 @@ var constants = bcoin.constants;
var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var Path = require('./path');
var Script = require('../script/script');
var MAX_POINT = String.fromCharCode(0xdbff, 0xdfff); // U+10FFFF
/*
@ -816,6 +817,8 @@ WalletDB.prototype.saveAddress = co(function* saveAddress(wid, rings) {
path = path.clone();
path.hash = ring.getProgramHash('hex');
path.version = -1;
path.type = Script.types.SCRIPTHASH;
yield this.writeAddress(wid, ring.getProgramAddress(), path);
}
@ -854,6 +857,54 @@ WalletDB.prototype.writeAddress = co(function* writeAddress(wid, address, path)
batch.put(layout.p(hash), serializePaths(paths));
});
/**
* Save paths to the path map.
* @param {WalletID} wid
* @param {Path[]} paths
* @returns {Promise}
*/
WalletDB.prototype.savePath = co(function* savePath(wid, paths) {
var i, path;
for (i = 0; i < paths.length; i++) {
path = paths[i];
yield this.writePath(wid, path);
}
});
/**
* Save a single address to the path map.
* @param {WalletID} wid
* @param {Path} path
* @returns {Promise}
*/
WalletDB.prototype.writePath = co(function* writePath(wid, path) {
var hash = path.hash;
var batch = this.batch(wid);
var paths;
if (this.filter)
this.filter.add(hash, 'hex');
this.emit('save address', path.toAddress(), path);
paths = yield this.getAddressPaths(hash);
if (!paths)
paths = {};
if (paths[wid])
return;
paths[wid] = path;
this.pathCache.set(hash, paths);
batch.put(layout.p(hash), serializePaths(paths));
});
/**
* Retrieve paths by hash.
* @param {Hash} hash
@ -964,7 +1015,9 @@ WalletDB.prototype.getWalletPaths = co(function* getWalletPaths(wid) {
return path;
}
});
yield this.fillPathNames(paths);
return paths;
});

View File

@ -934,6 +934,25 @@ describe('Wallet', function() {
assert(t2.inputs[0].prevout.hash === tx.hash('hex'));
}));
it('should import address', cob(function *() {
var key = bcoin.keyring.generate();
var w = yield walletdb.create();
var options, k, t1, t2, tx;
yield w.importAddress('default', key.getAddress());
k = yield w.getPath(key.getHash('hex'));
assert.equal(k.hash, key.getHash('hex'));
}));
it('should get details', cob(function *() {
var w = wallet;
var txs = yield w.getRange('foo', { start: 0xdeadbeef - 1000 });
var details = yield w.toDetails(txs);
assert.equal(details[0].toJSON().outputs[0].path.name, 'foo');
}));
it('should cleanup', cob(function *() {
var records = yield walletdb.dump();
constants.tx.COINBASE_MATURITY = 100;