diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index c5497e0e..779a6b2f 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -821,6 +821,26 @@ HD.isHD = function isHD(obj) { || HDPublicKey.isHDPublicKey(obj); }; +/** + * Test whether an object is an HD private key. + * @param {Object} obj + * @returns {Boolean} + */ + +HD.isPrivate = function isPrivate(obj) { + return HDPrivateKey.isHDPrivateKey(obj); +}; + +/** + * Test whether an object is an HD public key. + * @param {Object} obj + * @returns {Boolean} + */ + +HD.isPublic = function isPublic(obj) { + return HDPublicKey.isHDPublicKey(obj); +}; + /** * HDPrivateKey * @exports HDPrivateKey @@ -1223,6 +1243,46 @@ HDPrivateKey.prototype.equal = function equal(obj) { && utils.equal(this.privateKey, obj.privateKey); }; +/** + * Compare a key against an object. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPrivateKey.prototype.compare = function compare(key) { + var cmp; + + if (!HDPrivateKey.isHDPrivateKey(key)) + return 1; + + cmp = this.depth - key.depth; + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.parentFingerPrint, key.parentFingerPrint); + + if (cmp !== 0) + return cmp; + + cmp = this.childIndex - key.childIndex; + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.chainCode, key.chainCode); + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.privateKey, key.privateKey); + + if (cmp !== 0) + return cmp; + + return 0; +}; + /** * Inject properties from seed. * @private @@ -1838,6 +1898,46 @@ HDPublicKey.prototype.equal = function equal(obj) { && utils.equal(this.publicKey, obj.publicKey); }; +/** + * Compare a key against an object. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPublicKey.prototype.compare = function compare(key) { + var cmp; + + if (!HDPublicKey.isHDPublicKey(key)) + return 1; + + cmp = this.depth - key.depth; + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.parentFingerPrint, key.parentFingerPrint); + + if (cmp !== 0) + return cmp; + + cmp = this.childIndex - key.childIndex; + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.chainCode, key.chainCode); + + if (cmp !== 0) + return cmp; + + cmp = utils.cmp(this.publicKey, key.publicKey); + + if (cmp !== 0) + return cmp; + + return 0; +}; + /** * Convert key to a more json-friendly object. * @returns {Object} diff --git a/lib/bcoin/http/rpc.js b/lib/bcoin/http/rpc.js index 82c9aaa2..6798b5d6 100644 --- a/lib/bcoin/http/rpc.js +++ b/lib/bcoin/http/rpc.js @@ -1134,10 +1134,10 @@ RPC.prototype.getrawmempool = function getrawmempool(args, callback) { if (args.length > 0) verbose = toBool(args[0], false); - this.mempoolToJSON(verbose, callback); + this._mempoolToJSON(verbose, callback); }; -RPC.prototype.mempoolToJSON = function mempoolToJSON(verbose, callback) { +RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose, callback) { var out = {}; var i, hashes, hash, entry; @@ -3239,7 +3239,6 @@ RPC.prototype._listReceived = function _listReceived(minconf, empty, account, ca for (i = 0; i < paths.length; i++) { path = paths[i]; - hash = new Buffer(path.hash, 'hex'); map[path.hash] = { involvesWatchonly: false, address: path.toAddress().toBase58(self.network), diff --git a/lib/bcoin/keyring.js b/lib/bcoin/keyring.js index 26155e87..86417085 100644 --- a/lib/bcoin/keyring.js +++ b/lib/bcoin/keyring.js @@ -246,13 +246,12 @@ KeyRing.fromAccount = function fromAccount(account, key, keys, change, index) { KeyRing.prototype.addKey = function addKey(key) { assert(Buffer.isBuffer(key)); - if (utils.indexOf(this.keys, key) !== -1) - return; + utils.binaryInsert(this.keys, key, utils.cmp, true); - if (this.keys.length === this.n) + if (this.keys.length > this.n) { + utils.binaryRemove(this.keys, key, utils.cmp); throw new Error('Cannot add more keys.'); - - utils.binaryInsert(this.keys, key, utils.cmp); + } }; /** @@ -612,25 +611,12 @@ KeyRing.prototype.ownOutput = function ownOutput(tx, index) { * @returns {Number} Total number of scripts built. */ -KeyRing.prototype.scriptInputs = function scriptInputs(tx, index) { +KeyRing.prototype.scriptInputs = function scriptInputs(tx) { var total = 0; var i, input; - if (index && typeof index === 'object') - index = tx.inputs.indexOf(index); - for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; - - if (index != null && index !== i) - continue; - - if (!input.coin) - continue; - - if (!this.ownOutput(input.coin)) - continue; - if (tx.scriptInput(i, this)) total++; } @@ -643,32 +629,16 @@ KeyRing.prototype.scriptInputs = function scriptInputs(tx, index) { * to build/sign inputs that are redeemable by this address. * @param {MTX} tx * @param {HDPrivateKey|KeyPair|Buffer} key - Private key. - * @param {Number?} index - Index of input. If not present, - * it will attempt to build and sign all redeemable inputs. - * @param {SighashType?} type * @returns {Number} Total number of inputs scripts built and signed. */ -KeyRing.prototype.sign = function sign(tx, key, index, type) { +KeyRing.prototype.sign = function sign(tx, key) { var total = 0; var i, input; - if (index && typeof index === 'object') - index = tx.inputs.indexOf(index); - for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; - - if (index != null && index !== i) - continue; - - if (!input.coin) - continue; - - if (!this.ownOutput(input.coin)) - continue; - - if (tx.sign(i, this, key, type)) + if (tx.sign(i, this, key)) total++; } diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index b5c11382..621ad484 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -220,15 +220,13 @@ MTX.prototype.addOutput = function addOutput(options, value) { MTX.prototype.scriptInput = function scriptInput(index, ring) { var input, prev, n, i, vector, redeemScript, witnessScript; - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - // Get the input input = this.inputs[index]; assert(input); // We should have previous outputs by now. - assert(input.coin, 'Coins are not available for scripting.'); + if (!input.coin) + return false; // Optimization: Don't bother with any below // calculation if the output is already templated. @@ -391,11 +389,8 @@ MTX.prototype.scriptInput = function scriptInput(index, ring) { MTX.prototype.createSignature = function createSignature(index, prev, key, type, version) { var hash; - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - if (type == null) - type = 'all'; + type = constants.hashType.ALL; if (typeof type === 'string') type = constants.hashType[type.toUpperCase()]; @@ -423,15 +418,20 @@ MTX.prototype.signInput = function signInput(index, ring, key, type) { var input, prev, signature, keyIndex, signatures, i; var len, m, n, keys, vector, version; - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - // Get the input input = this.inputs[index]; assert(input); // We should have previous outputs by now. - assert(input.coin, 'Coins are not available for signing.'); + if (!input.coin) + return false; + + // Optimization: test output against the + // address map to avoid unnecessary calculation. + // A hash table lookup may be faster than all + // the nonsense below. + if (!ring.ownOutput(input.coin)) + return false; // Get the previous output's script prev = input.coin.script; @@ -712,12 +712,7 @@ MTX.prototype.isSigned = function isSigned() { */ MTX.prototype.sign = function sign(index, ring, key, type) { - var input; - - if (index && typeof index === 'object') - index = this.inputs.indexOf(index); - - input = this.inputs[index]; + var input = this.inputs[index]; assert(input); // Build script for input diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index dcfacd6c..a1edc1aa 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -1393,10 +1393,10 @@ TXDB.prototype.filterLocked = function filterLocked(coins) { for (i = 0; i < coins.length; i++) { coin = coins[i]; if (!this.isLocked(coin)) - out.push(coins); + out.push(coin); } - return coins; + return out; }; /** diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 5b7f82be..f27d4db3 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2256,14 +2256,21 @@ utils.binarySearch = function binarySearch(items, key, compare, insert) { * @returns {Number} index */ -utils.binaryInsert = function binaryInsert(items, item, compare) { +utils.binaryInsert = function binaryInsert(items, item, compare, uniq) { var i = utils.binarySearch(items, item, compare, true); + + if (uniq && i < items.length) { + if (compare(items[i], item) === 0) + return -1; + } + if (i === 0) items.unshift(item); else if (i === items.length) items.push(item); else items.splice(i, 0, item); + return i; }; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 4fae1249..a6171ec5 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -1360,8 +1360,6 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) { * sign, only creates signature slots). Only builds scripts * for inputs that are redeemable by this wallet. * @param {MTX} tx - * @param {Number?} index - Index of input. If not present, - * it will attempt to sign all redeemable inputs. * @param {Function} callback - Returns [Error, Number] * (total number of scripts built). */ @@ -1386,16 +1384,13 @@ Wallet.prototype.scriptInputs = function scriptInputs(tx, callback) { * to build/sign inputs that are redeemable by this wallet. * @param {MTX} tx * @param {Object|String|Buffer} options - Options or passphrase. - * @param {Number?} options.index - Index of input. If not present, - * it will attempt to build and sign all redeemable inputs. - * @param {SighashType?} options.type * @param {Function} callback - Returns [Error, Number] (total number * of inputs scripts built and signed). */ Wallet.prototype.sign = function sign(tx, options, callback) { var self = this; - var passphrase, timeout, index, type; + var passphrase, timeout; if (typeof options === 'function') { callback = options; @@ -1407,8 +1402,6 @@ Wallet.prototype.sign = function sign(tx, options, callback) { passphrase = options.passphrase; timeout = options.timeout; - index = options.index; - type = options.type; this.deriveInputs(tx, function(err, rings) { if (err) @@ -1418,34 +1411,11 @@ Wallet.prototype.sign = function sign(tx, options, callback) { if (err) return callback(err); - self._sign(rings, master, tx, index, type, callback); + self.signAsync(rings, master, tx, callback); }); }); }; -/** - * Sign a transaction. - * @param {KeyRing[]} rings - * @param {HDPrivateKey} master - * @param {MTX} tx - * @param {Number?} index - * @param {SighashType?} type - */ - -Wallet.sign = function sign(rings, master, tx, index, type) { - var total = 0; - var i, ring, key; - - for (i = 0; i < rings.length; i++) { - ring = rings[i]; - key = ring.derive(master); - assert(utils.equal(key.getPublicKey(), ring.key)); - total += ring.sign(tx, key, index, type); - } - - return total; -}; - /** * Sign a transaction asynchronously. * @param {KeyRing[]} rings @@ -1457,20 +1427,43 @@ Wallet.sign = function sign(rings, master, tx, index, type) { * of inputs scripts built and signed). */ -Wallet.prototype._sign = function _sign(rings, master, tx, index, type, callback) { +Wallet.prototype.signAsync = function signAsync(rings, master, tx, callback) { var result; if (!this.workerPool) { callback = utils.asyncify(callback); try { - result = Wallet.sign(rings, master, tx, index, type); + result = Wallet.sign(rings, master, tx); } catch (e) { return callback(e); } return callback(null, result); } - this.workerPool.sign(rings, master, tx, index, type, callback); + this.workerPool.sign(rings, master, tx, callback); +}; + +/** + * Sign a transaction. + * @param {KeyRing[]} rings + * @param {HDPrivateKey} master + * @param {MTX} tx + * @param {Number?} index + * @param {SighashType?} type + */ + +Wallet.sign = function sign(rings, master, tx) { + var total = 0; + var i, ring, key; + + for (i = 0; i < rings.length; i++) { + ring = rings[i]; + key = ring.derive(master); + assert(utils.equal(key.getPublicKey(), ring.key)); + total += ring.sign(tx, key); + } + + return total; }; /** @@ -2216,8 +2209,6 @@ Account.prototype.fromOptions = function fromOptions(options) { if (!this.name) this.name = this.accountIndex + ''; - this.pushKey(this.accountKey); - if (options.keys) { assert(Array.isArray(options.keys)); for (i = 0; i < options.keys.length; i++) @@ -2255,7 +2246,7 @@ Account.MAX_LOOKAHEAD = 5; Account.prototype.init = function init(callback) { // Waiting for more keys. - if (this.keys.length !== this.n) { + if (this.keys.length !== this.n - 1) { assert(!this.initialized); this.save(); return callback(); @@ -2292,40 +2283,29 @@ Account.prototype.open = function open(callback) { */ Account.prototype.pushKey = function pushKey(key) { - var index = -1; - var i; - - assert(key, 'Key required.'); - - if (key.accountKey) - key = key.accountKey; + var index; if (bcoin.hd.isExtended(key)) key = bcoin.hd.fromBase58(key); - if (key.hdPublicKey) - key = key.hdPublicKey; - - if (!bcoin.hd.isHD(key)) + if (!bcoin.hd.isPublic(key)) throw new Error('Must add HD keys to wallet.'); if (!key.isAccount44()) throw new Error('Must add HD account keys to BIP44 wallet.'); - for (i = 0; i < this.keys.length; i++) { - if (this.keys[i].equal(key)) { - index = i; - break; - } - } + if (key.equal(this.accountKey)) + throw new Error('Cannot add own key.'); - if (index !== -1) + index = utils.binaryInsert(this.keys, key, cmp, true); + + if (index === -1) return false; - if (this.keys.length === this.n) + if (this.keys.length > this.n - 1) { + utils.binaryRemove(this.keys, key, cmp); throw new Error('Cannot add more keys.'); - - this.keys.push(key); + } return true; }; @@ -2339,42 +2319,22 @@ Account.prototype.pushKey = function pushKey(key) { */ Account.prototype.spliceKey = function spliceKey(key) { - var index = -1; - var i; - - assert(key, 'Key required.'); - - if (key.accountKey) - key = key.accountKey; - if (bcoin.hd.isExtended(key)) key = bcoin.hd.fromBase58(key); - if (key.hdPublicKey) - key = key.hdPublicKey; - - if (!bcoin.hd.isHD(key)) + if (!bcoin.hd.isHDPublicKey(key)) throw new Error('Must add HD keys to wallet.'); if (!key.isAccount44()) throw new Error('Must add HD account keys to BIP44 wallet.'); - for (i = 0; i < this.keys.length; i++) { - if (this.keys[i].equal(key)) { - index = i; - break; - } - } + if (key.equal(this.accountKey)) + throw new Error('Cannot remove own key.'); - if (index === -1) - return false; - - if (this.keys.length === this.n) + if (this.keys.length === this.n - 1) throw new Error('Cannot remove key.'); - this.keys.splice(index, 1); - - return true; + return utils.binaryRemove(this.keys, key, cmp); }; /** @@ -2426,7 +2386,7 @@ Account.prototype._checkKeys = function _checkKeys(callback) { if (this.initialized || this.type !== keyTypes.MULTISIG) return callback(null, false); - if (this.keys.length !== this.n) + if (this.keys.length !== this.n - 1) return callback(null, false); ring = this.deriveReceive(0); @@ -2708,7 +2668,7 @@ Account.prototype.toJSON = function toJSON() { */ Account.prototype.fromJSON = function fromJSON(json) { - var i; + var i, key; assert.equal(json.network, this.network.type); assert(utils.isNumber(json.wid)); @@ -2738,8 +2698,10 @@ Account.prototype.fromJSON = function fromJSON(json) { assert(this.type != null); - for (i = 0; i < json.keys.length; i++) - this.keys.push(bcoin.hd.fromBase58(json.keys[i])); + for (i = 0; i < json.keys.length; i++) { + key = bcoin.hd.fromBase58(json.keys[i]); + this.pushKey(key); + } return this; }; @@ -2751,7 +2713,7 @@ Account.prototype.fromJSON = function fromJSON(json) { Account.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); - var i; + var i, key; p.writeU32(this.network.magic); p.writeVarString(this.name, 'utf8'); @@ -2766,8 +2728,10 @@ Account.prototype.toRaw = function toRaw(writer) { p.writeBytes(this.accountKey.toRaw()); p.writeU8(this.keys.length); - for (i = 0; i < this.keys.length; i++) - p.writeBytes(this.keys[i].toRaw()); + for (i = 0; i < this.keys.length; i++) { + key = this.keys[i]; + p.writeBytes(key.toRaw()); + } if (!writer) p = p.render(); @@ -2784,7 +2748,7 @@ Account.prototype.toRaw = function toRaw(writer) { Account.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - var i, count; + var i, count, key; this.network = bcoin.network.fromMagic(p.readU32()); this.name = p.readVarString('utf8'); @@ -2802,8 +2766,10 @@ Account.prototype.fromRaw = function fromRaw(data) { count = p.readU8(); - for (i = 0; i < count; i++) - this.keys.push(bcoin.hd.fromRaw(p.readBytes(82))); + for (i = 0; i < count; i++) { + key = bcoin.hd.fromRaw(p.readBytes(82)); + this.pushKey(key); + } return this; }; @@ -3279,6 +3245,14 @@ MasterKey.isMasterKey = function isMasterKey(obj) { && typeof obj.decrypt === 'function'; }; +/* + * Helpers + */ + +function cmp(key1, key2) { + return key1.compare(key2); +} + /* * Expose */ diff --git a/lib/bcoin/workers.js b/lib/bcoin/workers.js index 6b761087..24bef1a2 100644 --- a/lib/bcoin/workers.js +++ b/lib/bcoin/workers.js @@ -245,13 +245,11 @@ Workers.prototype.verify = function verify(tx, flags, callback) { * @param {KeyRing[]} rings * @param {HDPrivateKey} master * @param {MTX} tx - * @param {Number?} index - * @param {SighashType?} type * @param {Function} callback */ -Workers.prototype.sign = function sign(rings, master, tx, index, type, callback) { - var args = [rings, master, tx, index, type]; +Workers.prototype.sign = function sign(rings, master, tx, callback) { + var args = [rings, master, tx]; var i, input, sig, sigs, total; this.execute('sign', args, -1, function(err, result) { @@ -775,12 +773,10 @@ jobs.verify = function verify(tx, flags) { * @param {KeyRing[]} rings * @param {HDPrivateKey} master * @param {MTX} tx - * @param {Number?} index - * @param {SighashType?} type */ -jobs.sign = function sign(rings, master, tx, index, type) { - var total = bcoin.wallet.sign(rings, master, tx, index, type); +jobs.sign = function sign(rings, master, tx) { + var total = bcoin.wallet.sign(rings, master, tx); var sigs = []; var i, input;