From eacd1e2eced42aa962a4655fab5bbade10a74169 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 3 Feb 2016 19:44:14 -0800 Subject: [PATCH] bip45 wallet --- lib/bcoin/address.js | 59 ++++-- lib/bcoin/hd.js | 59 ++++-- lib/bcoin/keypair.js | 6 + lib/bcoin/utils.js | 6 + lib/bcoin/wallet.js | 438 ++++++++++++++++++++++++++++++++++++------- test/wallet-test.js | 53 +++--- 6 files changed, 494 insertions(+), 127 deletions(-) diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 7e5436fd..0d871639 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -42,18 +42,6 @@ function Address(options) { compressed: options.compressed }); - // Compatability - if (options.multisig) { - if (options.multisig.type) - options.type = options.multisig.type; - if (options.multisig.keys) - options.keys = options.multisig.keys; - if (options.multisig.m) - options.m = options.multisig.m; - if (options.multisig.n) - options.n = options.multisig.n; - } - this.type = options.type || 'pubkeyhash'; this.subtype = options.subtype; this.keys = []; @@ -141,8 +129,8 @@ Address.prototype.removeKey = function removeKey(key) { key = utils.toBuffer(key); - var index = this.keys.map(function(key, i) { - return utils.isEqual(key, pub) ? i : null; + var index = this.keys.map(function(pub, i) { + return utils.isEqual(pub, key) ? i : null; }).filter(function(i) { return i !== null; })[0]; @@ -226,9 +214,6 @@ Address.prototype.getScriptAddress = function getScriptAddress() { }; Address.prototype.getPublicKey = function getPublicKey(enc) { - if (!this.key.priv) - return; - if (!enc) { if (this._pub) return this._pub; @@ -241,6 +226,32 @@ Address.prototype.getPublicKey = function getPublicKey(enc) { return this.key.getPublic(enc); }; +Address.prototype.getHDPublicKey = function getPublicKey(enc) { + if (!this.key.hd) + return; + + if (enc === 'base58') + return this.key.hd.xpubkey; + + if (this.key.hd.isPublic) + return this.key.hd; + + return this.key.hd.hdpub; +}; + +Address.prototype.getHDPrivateKey = function getPublicKey(enc) { + if (!this.key.hd) + return; + + if (!this.key.hd.isPrivate) + return; + + if (enc === 'base58') + return this.key.hd.xprivkey; + + return this.key.hd; +}; + Address.prototype.getKeyHash = function getKeyHash() { if (this._hash) return this._hash; @@ -521,6 +532,20 @@ Address.prototype.__defineGetter__('address', function() { return this.getAddress(); }); +Address.prototype.__defineGetter__('realType', function() { + if (this.type === 'scripthash') + return this.subtype; + return this.type; +}); + +Address.prototype.__defineGetter__('hdPrivateKey', function() { + return this.getHDPrivateKey(); +}); + +Address.prototype.__defineGetter__('hdPublicKey', function() { + return this.getHDPublicKey(); +}); + Address.prototype.toJSON = function toJSON(encrypt) { return { v: 1, diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 2be834a8..4dce6773 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -252,12 +252,19 @@ HDPrivateKey.prototype.scan44 = function scan44(options, txByAddress, callback) })(0); }; -HDPrivateKey.prototype.deriveRoot44 = function deriveRoot44(options) { - var coinType = options.coinType; - var accountIndex = options.accountIndex; +HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(options) { + var coinType, accountIndex, child; - if (this instanceof HDPublicKey) + if (typeof options === 'number') + options = { accountIndex: options }; + + coinType = options.coinType; + accountIndex = options.accountIndex; + + if (this instanceof HDPublicKey) { + assert(this.isAccount44()); return this; + } if (coinType == null) coinType = network.type === 'main' ? 0 : 1; @@ -265,13 +272,17 @@ HDPrivateKey.prototype.deriveRoot44 = function deriveRoot44(options) { assert(utils.isFinite(coinType)); assert(utils.isFinite(accountIndex)); - return this + child = this .derive(44, true) .derive(coinType, true) .derive(accountIndex, true); + + assert(child.isAccount44()); + + return child; }; -HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options, isPublic) { +HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options) { var chain = options.chain; var addressIndex = options.addressIndex; @@ -282,7 +293,7 @@ HDPrivateKey.prototype.deriveBIP44 = function deriveBIP44(options, isPublic) { assert(utils.isFinite(addressIndex)); return this - .deriveRoot44(options) + .deriveAccount44(options) .derive(chain) .derive(addressIndex); }; @@ -317,7 +328,7 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) var keys = []; var root; - root = this.deriveRoot45(options); + root = this.derivePurpose45(options); return (function chainCheck(chainConstant) { return (function scanner(cosignerIndex) { @@ -372,10 +383,19 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) })(0); }; -HDPrivateKey.prototype.deriveRoot45 = function deriveRoot45(options) { - if (this instanceof HDPublicKey) +HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45(options) { + var child; + + if (this instanceof HDPublicKey) { + assert(this.isPurpose45()); return this; - return this.derive(45, true); + } + + child = this.derive(45, true); + + assert(child.isPurpose45()); + + return child; }; HDPrivateKey.prototype.deriveBIP45 = function deriveBIP45(options) { @@ -391,7 +411,7 @@ HDPrivateKey.prototype.deriveBIP45 = function deriveBIP45(options) { assert(utils.isFinite(addressIndex)); return this - .deriveRoot45(options) + .derivePurpose45(options) .derive(cosignerIndex) .derive(chain) .derive(addressIndex); @@ -413,6 +433,14 @@ HDPrivateKey.prototype.deriveCosignerAddress = function deriveCosignerAddress(co }); }; +HDPrivateKey.prototype.isPurpose45 = function isPurpose45(options) { + return new bn(this.childIndex).toNumber() === constants.hd.hardened + 45; +}; + +HDPrivateKey.prototype.isAccount44 = function isAccount44(options) { + return new bn(this.depth).toNumber() === 3; +}; + HDPrivateKey.getPath = function getPath(options) { var purpose, coinType, accountIndex, chain, addressIndex; @@ -765,17 +793,20 @@ function HDPublicKey(options) { } HDPublicKey.prototype.scan44 = HDPrivateKey.prototype.scan44; -HDPublicKey.prototype.deriveRoot44 = HDPrivateKey.prototype.deriveRoot44; +HDPublicKey.prototype.deriveAccount44 = HDPrivateKey.prototype.deriveAccount44; HDPublicKey.prototype.deriveBIP44 = HDPrivateKey.prototype.deriveBIP44; HDPublicKey.prototype.deriveChange = HDPrivateKey.prototype.deriveChange; HDPublicKey.prototype.deriveAddress = HDPrivateKey.prototype.deriveAddress; HDPublicKey.prototype.scan45 = HDPrivateKey.prototype.scan45; -HDPublicKey.prototype.deriveRoot45 = HDPrivateKey.prototype.deriveRoot45; +HDPublicKey.prototype.derivePurpose45 = HDPrivateKey.prototype.derivePurpose45; HDPublicKey.prototype.deriveBIP45 = HDPrivateKey.prototype.deriveBIP45; HDPublicKey.prototype.deriveCosignerChange = HDPrivateKey.prototype.deriveCosignerChange; HDPublicKey.prototype.deriveCosignerAddress = HDPrivateKey.prototype.deriveCosignerAddress; +HDPublicKey.prototype.isPurpose45 = HDPrivateKey.prototype.isPurpose45; +HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44; + HDPublicKey.isExtended = function isExtended(data) { if (typeof data !== 'string') return false; diff --git a/lib/bcoin/keypair.js b/lib/bcoin/keypair.js index 38d13a7f..7049e69e 100644 --- a/lib/bcoin/keypair.js +++ b/lib/bcoin/keypair.js @@ -33,6 +33,12 @@ function KeyPair(options) { this.hd = options.hd || null; this.compressed = options.compressed !== false; + if (options.privateKey) + options.priv = options.privateKey; + + if (options.publicKey) + options.pub = options.publicKey; + if (options.priv instanceof bcoin.hd.priv) { this.hd = options.priv; this._key = options.priv.pair; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index b8ad22d7..10cc3735 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -763,6 +763,12 @@ utils.sortKeys = function sortKeys(keys) { }); }; +utils.sortHDKeys = function sortHDKeys(keys) { + return keys.slice().sort(function(a, b) { + return new bn(a.publicKey).cmp(new bn(b.publicKey)) > 0; + }); +}; + utils.uniq = function(obj) { var out = []; var i = 0; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 2fd1bd0a..ff87724c 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -31,67 +31,323 @@ function Wallet(options) { options = utils.merge({}, options); + if (options.hd) { + options.master = options.hd !== true + ? bcoin.hd.priv(options.hd) + : bcoin.hd.priv(); + delete options.hd; + } + + if (options.priv + || options.pub + || options.key + || options.personalization + || options.entropy + || options.compressed) { + if ((options.key instanceof bcoin.hd.priv) + || options.key instanceof bcoin.hd.pub) { + options.master = options.key; + delete options.key; + } else if (options.priv instanceof bcoin.hd.priv) { + options.master = options.priv; + delete options.priv; + } else if (options.pub instanceof bcoin.hd.pub) { + options.master = options.pub; + delete options.pub; + } + } + this.options = options; this.addresses = []; this.master = options.master || null; + + if (this.master && !(this.master instanceof bcoin.keypair)) + this.master = bcoin.keypair({ hd: this.master }); + this._addressTable = {}; this._labelMap = {}; this.accountIndex = options.accountIndex || 0; this.addressDepth = options.addressDepth || 0; this.changeDepth = options.changeDepth || 0; + this.cosignerIndex = 0; + this.purposeKeys = options.purposeKeys || []; + this.keys = options.keys || []; + + this.hd = false; + this.hdpm = false; + this.multisig = false; + + this.type = options.type || 'pubkeyhash'; + this.subtype = options.subtype; + this.keys = []; + this.m = options.m || 1; + this.n = options.n || 1; + this.nmax = this.type === 'scripthash' + ? (this.compressed !== false ? 15 : 7) + : 3; + + if (this.n > 1) { + if (this.type !== 'multisig') + this.type = 'scripthash'; + if (this.type === 'scripthash') + this.subtype = 'multisig'; + } + + if (this.master) + this.hd = true; + + if (this.master && this.type === 'scripthash' && this.subtype === 'multisig') + this.hdpm = true; + + if (this.type === 'multisig' || this.subtype === 'multisig') + this.multisig = true; + + if (network.prefixes[this.type] == null) + throw new Error('Unknown prefix: ' + this.type); + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (this.n < 1 || this.n > this.nmax) + throw new Error('n ranges between 1 and ' + this.nmax); + + if (this.hdpm) { + this.purposeKey = this.master.hd.isPublic + ? this.master.hd + : this.master.hd.derivePurpose45(); + } else if (this.hd) { + this.accountKey = this.master.hd.isPublic + ? this.master.hd + : this.master.hd.deriveAccount44(this.accountIndex); + } if (!options.addresses) options.addresses = []; - if (!options.addresses.length) - options.addresses.push(utils.merge({}, options)); - - options.addresses.forEach(function(address) { - this.addAddress(address); - }, this); - - // Create a non-master account address if we don't have one. - if (this.master) { - for (i = 0; i < this.addresses.length; i++) { - if (this.addresses[i].key.hd && !this.addresses[i].change) - break; - } - if (i === this.addresses.length) - this.createNewAddress(this._cleanOptions(options.addresses[0])); + if (options.priv + || options.pub + || options.key + || options.personalization + || options.entropy + || options.compressed) { + options.addresses.push({ + priv: options.priv, + pub: options.pub, + key: options.key, + personalization: options.personalization, + entropy: options.entropy, + compressed: options.compressed, + type: this.type, + subtype: this.subtype, + m: this.m, + n: this.n, + keys: [], + change: false + }); } - // Find the last change address if there is one. - for (i = this.addresses.length - 1; i >= 0; i--) { - if (this.addresses[i].change) - break; - } - - if (i === -1) - this.changeAddress = this.createChangeAddress(); - else - this.changeAddress = this.addresses[i]; - this.storage = options.storage; this.loading = true; this.lastTs = 0; + if (!this.hdpm) { + if (options.addresses.length) { + this.current = bcoin.address(options.addresses[options.addresses.length - 1]); + this._firstKey = { + priv: this.current.key._key.getPrivate().toArray(), + pub: this.current.key._key.getPublic(true, 'array') + }; + } else { + this._firstKey = this.createKey(false, Math.max(0, this.addressDepth - 1)); + this.current = bcoin.address({ priv: this._firstKey.priv }); + } + } + + if (this.hdpm) + this.addKey(this.purposeKey); + else + this.addKey(this.current.publicKey); + + (options.keys || []).forEach(function(key) { + this.addKey(key); + }, this); +} + +inherits(Wallet, EventEmitter); + +Wallet.prototype._initAddresses = function() { + var options = this.options; + + options.addresses.forEach(function(address) { + address = this.addAddress(address); + if (!address.change) + this.current = address; + }, this); + + if (this.hd) { + for (i = 0; i < this.addressDepth; i++) + this.createAddress(false, i); + + for (i = 0; i < this.changeDepth; i++) + this.createAddress(true, i); + } + + // Create a non-master account address if we don't have one. + if (this.addresses.length === 0) + this.createAddress(); + + // Find the last change address if there is one. + if (this.hd) { + if (this.changeDepth === 0) + this.changeAddress = this.createAddress(true); + else + this.changeAddress = this.addresses[this.addresses.length - 1]; + } else { + for (i = this.addresses.length - 1; i >= 0; i--) { + if (this.addresses[i].change) + break; + } + + if (i === -1) + this.changeAddress = this.createAddress(true); + else + this.changeAddress = this.addresses[i]; + } + + assert(this.current); + assert(!this.current.change); + assert(this.changeAddress.change); + this.prefix = 'bt/wallet/' + this.getKeyAddress() + '/'; this.tx = new bcoin.txPool(this); this._init(); -} +}; -inherits(Wallet, EventEmitter); +Wallet.prototype.addKey = function addKey(key) { + var hdKey, has, i; -Wallet.prototype._cleanOptions = function _cleanOptions(options) { - return utils.merge(options, { - key: null, - priv: null, - pub: null, - hd: null + if (bcoin.hd.priv.isExtended(key)) + key = bcoin.hd.priv(key); + else if (bcoin.hd.pub.isExtended(key)) + key = bcoin.hd.pub(key); + + if (key instanceof bcoin.keypair) + key = key.hd; + + if (key instanceof bcoin.hd.priv) + key = key.hdpub; + + if (key instanceof bcoin.hd.pub) { + hdKey = key; + key = hdKey.publicKey; + } + + if (this.hdpm) { + if (!hdKey || !hdKey.isPurpose45()) + throw new Error('Must add HD purpose keys to HD wallet.'); + + has = this.purposeKeys.some(function(pub) { + return pub.xpubkey === hdKey.xpubkey; + }); + + if (has) + return; + + this.purposeKeys.push(hdKey); + + if (this.purposeKeys.length === this.n) + this.finalizeKeys(); + + return; + } + + key = utils.toBuffer(key); + + has = this.keys.some(function(k) { + return utils.isEqual(k, key); }); + + if (has) + return; + + this.keys.push(key); + + if (this.keys.length === this.n) + this.finalizeKeys(); +}; + +Wallet.prototype.finalizeKeys = function finalizeKeys(key) { + if (this.hdpm) { + this.purposeKeys = utils.sortHDKeys(this.purposeKeys); + + for (i = 0; i < this.purposeKeys.length; i++) { + if (utils.isEqual(this.purposeKeys[i].publicKey, this.purposeKey.publicKey)) { + this.cosignerIndex = i; + break; + } + } + + this._initAddresses(); + return; + } + + this.keys = utils.sortKeys(this.keys); + + this._initAddresses(); +}; + +Wallet.prototype.removeKey = function removeKey(key) { + var hdKey, index; + + if (bcoin.hd.priv.isExtended(key)) + key = bcoin.hd.priv(key); + else if (bcoin.hd.pub.isExtended(key)) + key = bcoin.hd.pub(key); + + if (key instanceof bcoin.keypair) + key = key.hd; + + if (key instanceof bcoin.hd.priv) + key = key.hdpub; + + if (key instanceof bcoin.hd.pub) { + hdKey = key; + key = hd.publicKey; + } + + if (this.hdpm) { + if (!hdKey || !hdKey.isPurpose45()) + throw new Error('Must add HD purpose keys to HD wallet.'); + + index = this.purposeKeys.map(function(pub, i) { + return pub.xpubkey === hdKey.xpubkey ? i : null; + }).filter(function(i) { + return i !== null; + })[0]; + + if (index == null) + return; + + this.purposeKeys.splice(index, 1); + + return; + } + + key = utils.toBuffer(key); + + index = this.keys.map(function(pub, i) { + return utils.isEqual(pub, key) ? i : null; + }).filter(function(i) { + return i !== null; + })[0]; + + if (index == null) + return; + + this.keys.splice(index, 1); }; Wallet.prototype._init = function init() { @@ -134,7 +390,7 @@ Wallet.prototype._init = function init() { }; Wallet.prototype.__defineGetter__('primary', function() { - return this.addresses[0]; + return this.current; }); Wallet.prototype._getAddressTable = function() { @@ -171,30 +427,52 @@ Wallet.prototype._addressIndex = function _addressIndex(address) { return -1; }; -Wallet.prototype.createChangeAddress = function createChangeAddress(options) { - if (!options) - options = {}; +Wallet.prototype.createAddress = function createAddress(change, index) { + var self = this; + var key = this.createKey(change, index); + var address; - options.change = true; + var options = { + priv: key.priv, + pub: key.pub, + type: this.type, + subtype: this.subtype, + m: this.m, + n: this.n, + keys: [], + change: change + }; - if (this.master) { - options.priv = - this.master.key.hd.deriveChange(this.accountIndex, this.changeDepth++); + if (this.hdpm) { + this.purposeKeys.forEach(function(key, cosignerIndex) { + key = key + .derive(cosignerIndex) + .derive(change ? 1 : 0) + .derive(change ? self.changeDepth : self.addressDepth); + options.keys.push(key.publicKey); + }); + this.keys = utils.sortKeys(options.keys); + } else { + this.keys.forEach(function(key, i) { + options.keys.push(key); + }); } - return this.addAddress(options); -}; - -Wallet.prototype.createNewAddress = function createNewAddress(options) { - if (!options) - options = {}; - - if (this.master) { - options.priv = - this.master.key.hd.deriveAddress(this.accountIndex, this.addressDepth++); + if (index == null) { + if (this.hd) { + if (change) + this.changeDepth++; + else + this.addressDepth++; + } } - return this.addAddress(options); + address = this.addAddress(options); + + if (!change) + this.current = address; + + return address; }; Wallet.prototype.hasAddress = function hasAddress(address) { @@ -220,12 +498,6 @@ Wallet.prototype.addAddress = function addAddress(address) { if (this._addressIndex(address) !== -1) return; - if (address.key.hd && address.key.hd.isMaster) { - assert(!this.master); - this.master = address; - return; - } - if (address._wallet) address._wallet.removeAddress(address); @@ -282,14 +554,6 @@ Wallet.prototype.removeAddress = function removeAddress(address) { return address; }; -Wallet.prototype.addKey = function addKey(key, i) { - return this.primary.addKey(key); -}; - -Wallet.prototype.removeKey = function removeKey(key) { - return this.primary.removeKey(key); -}; - Wallet.prototype.getPrivateKey = function getPrivateKey(enc) { return this.primary.getPrivateKey(enc); }; @@ -305,13 +569,49 @@ Wallet.prototype.getScripthash = function getScripthash() { Wallet.prototype.getScriptAddress = Wallet.prototype.getScriptaddress = function getScriptaddress() { - return this.primary.getScriptAddress(); + return this.current.getScriptAddress(); }; Wallet.prototype.getPublicKey = function getPublicKey(enc) { return this.primary.getPublicKey(enc); }; +Wallet.prototype.createKey = function createKey(change, index) { + var key, pub, priv; + + if (!this.hd) { + if (this._firstKey) { + key = this._firstKey; + delete this._firstKey; + return key; + } + key = bcoin.ecdsa.genKeyPair(); + return { + priv: key.getPrivate().toArray(), + pub: key.getPublic(true, 'array') + }; + } + + if (index == null) + index = change ? this.changeDepth : this.addressDepth; + + if (this.hdpm) { + key = this.purposeKey + .derive(this.cosignerIndex) + .derive(change ? 1 : 0) + .derive(index); + } else { + key = this.accountKey + .derive(change ? 1 : 0) + .derive(index); + } + + return { + priv: key.privateKey, + pub: key.publicKey + }; +}; + Wallet.prototype.getKeyHash = Wallet.prototype.getKeyhash = function getKeyhash() { return this.primary.getKeyHash(); @@ -327,7 +627,7 @@ Wallet.prototype.getHash = function getHash() { }; Wallet.prototype.getAddress = function getAddress() { - return this.primary.getAddress(); + return this.current.getAddress(); }; Wallet.prototype.ownInput = function ownInput(tx, index) { diff --git a/test/wallet-test.js b/test/wallet-test.js index 80c86378..dc0fcb55 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -74,11 +74,9 @@ describe('Wallet', function() { it('should multisign/verify TX', function() { var w = bcoin.wallet({ - multisig: { - type: 'multisig', - m: 1, - n: 2 - } + type: 'multisig', + m: 1, + n: 2 }); // var k2 = w.getPublicKey().concat(1); var k2 = bcoin.ecdsa.genKeyPair().getPublic(true, 'array'); @@ -265,37 +263,38 @@ describe('Wallet', function() { it('should verify 2-of-3 p2sh tx', function(cb) { // Create 3 2-of-3 wallets with our pubkeys as "shared keys" var w1 = bcoin.wallet({ - multisig: { - type: 'scripthash', - m: 2, - n: 3 - } + hd: true, + type: 'scripthash', + subtype: 'multisig', + m: 2, + n: 3 }); + var w2 = bcoin.wallet({ - multisig: { - type: 'scripthash', - m: 2, - n: 3 - } + hd: true, + type: 'scripthash', + subtype: 'multisig', + m: 2, + n: 3 }); + var w3 = bcoin.wallet({ hd: true, - multisig: { - type: 'scripthash', - m: 2, - n: 3 - } + type: 'scripthash', + subtype: 'multisig', + m: 2, + n: 3 }); - w3 = bcoin.wallet.fromJSON(w3.toJSON()); + // w3 = bcoin.wallet.fromJSON(w3.toJSON()); var receive = bcoin.wallet(); - w1.addKey(w2.getPublicKey()); - w1.addKey(w3.getPublicKey()); - w2.addKey(w1.getPublicKey()); - w2.addKey(w3.getPublicKey()); - w3.addKey(w1.getPublicKey()); - w3.addKey(w2.getPublicKey()); + w1.addKey(w2.purposeKey); + w1.addKey(w3.purposeKey); + w2.addKey(w1.purposeKey); + w2.addKey(w3.purposeKey); + w3.addKey(w1.purposeKey); + w3.addKey(w2.purposeKey); // Our p2sh address var addr = w1.getAddress();