From 2544e5310a13234fc4899e15a76fb4f4f48e952f Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 30 Sep 2016 23:46:13 -0700 Subject: [PATCH] walletdb: path refactor. --- lib/wallet/account.js | 56 +++++++----- lib/wallet/path.js | 190 +++++++++++++++++++++++++++-------------- lib/wallet/wallet.js | 82 ++++++++++++++++-- lib/wallet/walletdb.js | 53 ++++++++++++ test/wallet-test.js | 19 +++++ 5 files changed, 307 insertions(+), 93 deletions(-) diff --git a/lib/wallet/account.js b/lib/wallet/account.js index fff7702c..205aa672 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -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 diff --git a/lib/wallet/path.js b/lib/wallet/path.js index a04fb1c6..062231e2 100644 --- a/lib/wallet/path.js +++ b/lib/wallet/path.js @@ -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} diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index bda615cd..b000f3b4 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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) diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 57bf9cf2..7d6e58ff 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -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; }); diff --git a/test/wallet-test.js b/test/wallet-test.js index d43ea5fd..fda77f77 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -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;