diff --git a/lib/bcoin/chain/chainentry.js b/lib/bcoin/chain/chainentry.js index 51b3aa05..049ff1bd 100644 --- a/lib/bcoin/chain/chainentry.js +++ b/lib/bcoin/chain/chainentry.js @@ -14,7 +14,6 @@ var utils = require('../utils/utils'); var assert = utils.assert; var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); -var InvItem = bcoin.packets.InvItem; /** * Represents an entry in the chain. Unlike @@ -633,7 +632,7 @@ ChainEntry.prototype.toHeaders = function toHeaders() { */ ChainEntry.prototype.toInv = function toInv() { - return new InvItem(constants.inv.BLOCK, this.hash); + return new bcoin.invitem(constants.inv.BLOCK, this.hash); }; /** diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index d69bfc4f..aafa378c 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -136,17 +136,22 @@ function Environment() { this.ldb = require('./db/ldb'); this.rbt = require('./db/rbt'); + // Script + this.script = require('./script/script'); + this.opcode = require('./script/opcode'); + this.stack = require('./script/stack'); + this.witness = require('./script/witness'); + this.program = require('./script/program'); + this.sc = require('./script/sigcache'); + // Primitives this.bloom = require('./primitives/bloom'); - this.script = require('./primitives/script'); - this.opcode = this.script.Opcode; - this.stack = this.script.Stack; - this.witness = this.script.Witness; this.address = require('./primitives/address'); + this.outpoint = require('./primitives/outpoint'); this.input = require('./primitives/input'); - this.outpoint = this.input.Outpoint; this.output = require('./primitives/output'); this.coin = require('./primitives/coin'); + this.invitem = require('./primitives/invitem'); this.tx = require('./primitives/tx'); this.mtx = require('./primitives/mtx'); this.abstractblock = require('./primitives/abstractblock'); @@ -155,7 +160,9 @@ function Environment() { this.merkleblock = require('./primitives/merkleblock'); this.headers = require('./primitives/headers'); this.keyring = require('./primitives/keyring'); - this.hd = require('./primitives/hd'); + + // HD + this.hd = require('./hd/hd'); // Node this.logger = require('./node/logger'); @@ -167,7 +174,6 @@ function Environment() { // Net this.timedata = require('./net/timedata'); this.packets = require('./net/packets'); - this.invitem = this.packets.InvItem; this.bip150 = require('./net/bip150'); this.bip151 = require('./net/bip151'); this.bip152 = require('./net/bip152'); @@ -191,11 +197,10 @@ function Environment() { this.minerblock = require('./miner/minerblock'); // Wallet - this.txdb = require('./wallet/txdb'); this.wallet = require('./wallet/wallet'); - this.account = this.wallet.Account; + this.account = require('./wallet/account'); this.walletdb = require('./wallet/walletdb'); - this.path = this.walletdb.Path; + this.path = require('./wallet/path'); // HTTP this.http = require('./http'); @@ -203,9 +208,6 @@ function Environment() { // Workers this.workers = require('./workers/workers'); - // Sigcache - this.sc = require('./sigcache'); - // Global Instances this.sigcache = new this.sc(0); this.time = new this.timedata(); diff --git a/lib/bcoin/hd/README.md b/lib/bcoin/hd/README.md new file mode 100644 index 00000000..a0d6b080 --- /dev/null +++ b/lib/bcoin/hd/README.md @@ -0,0 +1,74 @@ +HD mnemonics and keys (BIP32, BIP39) for bcoin. + +Parts of this software were originally based on bitcore-lib: + +- https://github.com/bitpay/bitcore-lib/blob/master/lib/hdprivatekey.js +- https://github.com/bitpay/bitcore-lib/blob/master/lib/hdpublickey.js +- https://github.com/ryanxcharles/fullnode/blob/master/lib/bip32.js +- https://github.com/bitpay/bitcore-mnemonic/blob/master/lib/mnemonic.js + +BIP32 + +``` +Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). +https://github.com/bcoin-org/bcoin + +Copyright (c) 2013-2015 BitPay, Inc. + +Parts of this software are based on Bitcoin Core +Copyright (c) 2009-2015 The Bitcoin Core developers + +Parts of this software are based on fullnode +Copyright (c) 2014 Ryan X. Charles +Copyright (c) 2014 reddit, Inc. + +Parts of this software are based on BitcoinJS +Copyright (c) 2011 Stefan Thomas + +Parts of this software are based on BitcoinJ +Copyright (c) 2011 Google Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +BIP39 + +``` +The MIT License (MIT) + +Copyright (c) 2014 BitPay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/lib/bcoin/hd/hd.js b/lib/bcoin/hd/hd.js new file mode 100644 index 00000000..41595638 --- /dev/null +++ b/lib/bcoin/hd/hd.js @@ -0,0 +1,253 @@ +/*! + * hd.js - hd keys for bcoin + * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var assert = utils.assert; +var constants = bcoin.constants; +var LRU = require('../utils/lru'); +var Mnemonic = require('./mnemonic'); +var HDPrivateKey = require('./private'); +var HDPublicKey = require('./public'); + +/** + * @exports HD + */ + +var HD = exports; + +/** + * Instantiate an HD key (public or private) from an base58 string. + * @param {Base58String} xkey + * @returns {HDPrivateKey|HDPublicKey} + */ + +HD.fromBase58 = function fromBase58(xkey) { + if (HDPrivateKey.isExtended(xkey)) + return HDPrivateKey.fromBase58(xkey); + return HDPublicKey.fromBase58(xkey); +}; + +/** + * Generate an {@link HDPrivateKey}. + * @param {Object} options + * @param {Buffer?} options.privateKey + * @param {Buffer?} options.entropy + * @param {String?} network + * @returns {HDPrivateKey} + */ + +HD.generate = function generate(options, network) { + return HDPrivateKey.generate(options, network); +}; + +/** + * Generate an {@link HDPrivateKey} from a seed. + * @param {Object|Mnemonic|Buffer} options - seed, + * mnemonic, mnemonic options. + * @param {String?} network + * @returns {HDPrivateKey} + */ + +HD.fromSeed = function fromSeed(options, network) { + return HDPrivateKey.fromSeed(options, network); +}; + +/** + * Instantiate an hd private key from a mnemonic. + * @param {Mnemonic|Object} mnemonic + * @param {String?} network + * @returns {HDPrivateKey} + */ + +HD.fromMnemonic = function fromMnemonic(options, network) { + return HDPrivateKey.fromMnemonic(options, network); +}; + +/** + * Instantiate an HD key from a jsonified key object. + * @param {Object} json - The jsonified transaction object. + * @returns {HDPrivateKey|HDPublicKey} + */ + +HD.fromJSON = function fromJSON(json) { + if (json.xprivkey) + return HDPrivateKey.fromJSON(json); + return HDPublicKey.fromJSON(json); +}; + +/** + * Instantiate an HD key from serialized data. + * @param {Buffer} data + * @returns {HDPrivateKey|HDPublicKey} + */ + +HD.fromRaw = function fromRaw(data) { + if (HDPrivateKey.hasPrefix(data)) + return HDPrivateKey.fromRaw(data); + return HDPublicKey.fromRaw(data); +}; + +/** + * Instantiate HD key from extended serialized data. + * @param {Buffer} data + * @returns {HDPrivateKey|HDPublicKey} + */ + +HD.fromExtended = function fromExtended(data) { + if (HDPrivateKey.hasPrefix(data)) + return HDPrivateKey.fromExtended(data); + return HDPublicKey.fromRaw(data); +}; + +/** + * Generate an hdkey from any number of options. + * @param {Object|Mnemonic|Buffer} options - mnemonic, mnemonic + * options, seed, or base58 key. + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey|HDPublicKey} + */ + +HD.from = function from(options, network) { + var xkey; + + assert(options, 'Options required.'); + + if (options.xkey) + xkey = options.xkey; + else if (options.xpubkey) + xkey = options.xpubkey; + else if (options.xprivkey) + xkey = options.xprivkey; + else + xkey = options; + + if (HD.isExtended(xkey)) + return HD.fromBase58(xkey); + + if (HD.hasPrefix(options)) + return HD.fromRaw(options); + + return HD.fromMnemonic(options, network); +}; + +/** + * Test whether an object is in the form of a base58 hd key. + * @param {String} data + * @returns {Boolean} + */ + +HD.isExtended = function isExtended(data) { + return HDPrivateKey.isExtended(data) + || HDPublicKey.isExtended(data); +}; + +/** + * Test whether an object is in the form of a serialized hd key. + * @param {Buffer} data + * @returns {NetworkType} + */ + +HD.hasPrefix = function hasPrefix(data) { + return HDPrivateKey.hasPrefix(data) + || HDPublicKey.hasPrefix(data); +}; + +/** + * Parse a derivation path and return an array of indexes. + * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + * @param {String} path + * @param {Number?} max - Max index. + * @returns {Number[]} + */ + +HD.parsePath = function parsePath(path, max) { + var parts = path.split('/'); + var root = parts.shift(); + var result = []; + var i, hardened, index; + + if (max == null) + max = constants.hd.MAX_INDEX; + + if (root !== 'm' + && root !== 'M' + && root !== 'm\'' + && root !== 'M\'') { + throw new Error('Bad path root.'); + } + + for (i = 0; i < parts.length; i++) { + index = parts[i]; + hardened = index[index.length - 1] === '\''; + + if (hardened) + index = index.slice(0, -1); + + if (!/^\d+$/.test(index)) + throw new Error('Non-number path index.'); + + index = parseInt(index, 10); + + if (hardened) + index += constants.hd.HARDENED; + + if (!(index >= 0 && index < max)) + throw new Error('Index out of range.'); + + result.push(index); + } + + return result; +}; + +/** + * LRU cache to avoid deriving keys twice. + * @type {LRU} + */ + +HD.cache = new LRU(500); + +/** + * Test whether an object is an HD key. + * @param {Object} obj + * @returns {Boolean} + */ + +HD.isHD = function isHD(obj) { + return HDPrivateKey.isHDPrivateKey(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); +}; + +/* + * Expose + */ + +HD.Mnemonic = Mnemonic; +HD.PrivateKey = HDPrivateKey; +HD.PublicKey = HDPublicKey; diff --git a/lib/bcoin/hd/mnemonic.js b/lib/bcoin/hd/mnemonic.js new file mode 100644 index 00000000..9bb0f787 --- /dev/null +++ b/lib/bcoin/hd/mnemonic.js @@ -0,0 +1,542 @@ +/*! + * mnemonic.js - hd mnemonics for bcoin + * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var ec = require('../crypto/ec'); +var assert = utils.assert; +var constants = bcoin.constants; +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var HD = require('./hd'); +var unorm; + +/** + * HD Mnemonic + * @exports Mnemonic + * @constructor + * @param {Object} options + * @param {Number?} options.bit - Bits of entropy (Must + * be a multiple of 8) (default=128). + * @param {Buffer?} options.entropy - Entropy bytes. Will + * be generated with `options.bits` bits of entropy + * if not present. + * @param {String?} options.phrase - Mnemonic phrase (will + * be generated if not present). + * @param {String?} options.passphrase - Optional salt for + * key stretching (empty string if not present). + * @param {String?} options.language - Language. + */ + +function Mnemonic(options) { + if (!(this instanceof Mnemonic)) + return new Mnemonic(options); + + this.bits = constants.hd.MIN_ENTROPY; + this.language = 'english'; + this.entropy = null; + this.phrase = null; + this.passphrase = ''; + + if (options) + this.fromOptions(options); +} + +/** + * List of languages. + * @const {String[]} + * @default + */ + +Mnemonic.languages = [ + 'simplified chinese', + 'traditional chinese', + 'english', + 'french', + 'italian', + 'japanese' +]; + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +Mnemonic.prototype.fromOptions = function fromOptions(options) { + if (typeof options === 'string') + options = { phrase: options }; + + if (options.bits != null) { + assert(utils.isNumber(options.bits)); + assert(options.bits >= constants.hd.MIN_ENTROPY); + assert(options.bits <= constants.hd.MAX_ENTROPY); + assert(options.bits % 32 === 0); + this.bits = options.bits; + } + + if (options.language) { + assert(typeof options.language === 'string'); + assert(Mnemonic.languages.indexOf(options.language) !== -1); + this.language = options.language; + } + + if (options.passphrase) { + assert(typeof options.passphrase === 'string'); + this.passphrase = options.passphrase; + } + + if (options.phrase) { + this.fromPhrase(options.phrase); + return this; + } + + if (options.entropy) { + this.fromEntropy(options.entropy); + return this; + } + + return this; +}; + +/** + * Instantiate mnemonic from options. + * @param {Object} options + * @returns {Mnemonic} + */ + +Mnemonic.fromOptions = function fromOptions(options) { + return new Mnemonic().fromOptions(options); +}; + +/** + * Destroy the mnemonic (zeroes entropy). + */ + +Mnemonic.prototype.destroy = function destroy() { + this.bits = constants.hd.MIN_ENTROPY; + this.language = 'english'; + if (this.entropy) { + this.entropy.fill(0); + this.entropy = null; + } + this.phrase = null; + this.passphrase = ''; +}; + +/** + * Generate the seed. + * @param {String?} passphrase + * @returns {Buffer} pbkdf2 seed. + */ + +Mnemonic.prototype.toSeed = function toSeed(passphrase) { + if (!passphrase) + passphrase = this.passphrase; + + this.passphrase = passphrase; + + return utils.pbkdf2Sync( + nfkd(this.getPhrase()), + nfkd('mnemonic' + passphrase), + 2048, 64, 'sha512'); +}; + +/** + * Generate seed and create an hd private key. + * @param {String?} passphrase + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +Mnemonic.prototype.toKey = function toKey(passphrase, network) { + var seed = this.toSeed(passphrase); + var key = HD.PrivateKey.fromSeed(seed, network); + key.mnemonic = this; + return key; +}; + +/** + * Get or generate entropy. + * @returns {Buffer} + */ + +Mnemonic.prototype.getEntropy = function getEntropy() { + if (!this.entropy) + this.entropy = ec.random(this.bits / 8); + + assert(this.bits / 8 === this.entropy.length); + + return this.entropy; +}; + +/** + * Generate a mnemonic phrase from chosen language. + * @returns {String} + */ + +Mnemonic.prototype.getPhrase = function getPhrase() { + var i, j, phrase, wordlist, bits, entropy, index, pos, oct, bit; + + if (this.phrase) + return this.phrase; + + phrase = []; + wordlist = Mnemonic.getWordlist(this.language); + + entropy = this.getEntropy(); + bits = this.bits; + + // Append the hash to the entropy to + // make things easy when grabbing + // the checksum bits. + entropy = Buffer.concat([entropy, utils.sha256(entropy)]); + + // Include the first `ENT / 32` bits + // of the hash (the checksum). + bits += bits / 32; + + // Build the mnemonic by reading + // 11 bit indexes from the entropy. + for (i = 0; i < bits / 11; i++) { + index = 0; + for (j = 0; j < 11; j++) { + pos = i * 11 + j; + bit = pos % 8; + oct = (pos - bit) / 8; + index <<= 1; + index |= (entropy[oct] >>> (7 - bit)) & 1; + } + phrase.push(wordlist[index]); + } + + // Japanese likes double-width spaces. + if (this.language === 'japanese') + phrase = phrase.join('\u3000'); + else + phrase = phrase.join(' '); + + this.phrase = phrase; + + return phrase; +}; + +/** + * Inject properties from phrase. + * @private + * @param {String} phrase + */ + +Mnemonic.prototype.fromPhrase = function fromPhrase(phrase) { + var i, j, bits, pos, oct, bit, b, ent, entropy, lang; + var chk, word, wordlist, index, cbits, cbytes, words; + + assert(typeof phrase === 'string'); + + words = phrase.split(/[ \u3000]+/); + bits = words.length * 11; + cbits = bits % 32; + cbytes = Math.ceil(cbits / 8); + bits -= cbits; + + assert(bits >= constants.hd.MIN_ENTROPY); + assert(bits <= constants.hd.MAX_ENTROPY); + assert(bits % 32 === 0); + assert(cbits !== 0, 'Invalid checksum.'); + + ent = new Buffer(Math.ceil((bits + cbits) / 8)); + ent.fill(0); + + lang = Mnemonic.getLanguage(words[0]); + wordlist = Mnemonic.getWordlist(lang); + + for (i = 0; i < words.length; i++) { + word = words[i]; + index = wordlist.indexOf(word); + + if (index === -1) + throw new Error('Could not find word.'); + + for (j = 0; j < 11; j++) { + pos = i * 11 + j; + bit = pos % 8; + oct = (pos - bit) / 8; + b = (index >>> (10 - j)) & 1; + ent[oct] |= b << (7 - bit); + } + } + + entropy = ent.slice(0, ent.length - cbytes); + ent = ent.slice(ent.length - cbytes); + chk = utils.sha256(entropy); + + for (i = 0; i < cbits; i++) { + bit = i % 8; + oct = (i - bit) / 8; + b = (ent[oct] >>> (7 - bit)) & 1; + j = (chk[oct] >>> (7 - bit)) & 1; + if (b !== j) + throw new Error('Invalid checksum.'); + } + + assert(bits / 8 === entropy.length); + + this.bits = bits; + this.language = lang; + this.entropy = entropy; + this.phrase = phrase; + + return this; +}; + +/** + * Instantiate mnemonic from a phrase (validates checksum). + * @param {String} phrase + * @returns {Mnemonic} + * @throws on bad checksum + */ + +Mnemonic.fromPhrase = function fromPhrase(phrase) { + return new Mnemonic().fromPhrase(phrase); +}; + +/** + * Inject properties from entropy. + * @private + * @param {Buffer} entropy + * @param {String?} lang + */ + +Mnemonic.prototype.fromEntropy = function fromEntropy(entropy, lang) { + assert(Buffer.isBuffer(entropy)); + assert(entropy.length * 8 >= constants.hd.MIN_ENTROPY); + assert(entropy.length * 8 <= constants.hd.MAX_ENTROPY); + assert((entropy.length * 8) % 32 === 0); + assert(!lang || Mnemonic.languages.indexOf(lang) !== -1); + + this.entropy = entropy; + this.bits = entropy.length * 8; + + if (lang) + this.language = lang; + + return this; +}; + +/** + * Instantiate mnemonic from entropy. + * @param {Buffer} entropy + * @param {String?} lang + * @returns {Mnemonic} + */ + +Mnemonic.fromEntropy = function fromEntropy(entropy, lang) { + return new Mnemonic().fromEntropy(entropy, lang); +}; + +/** + * Determine a single word's language. + * @param {String} word + * @returns {String} Language. + * @throws on not found. + */ + +Mnemonic.getLanguage = function getLanguage(word) { + var i, lang, wordlist; + + for (i = 0; i < Mnemonic.languages.length; i++) { + lang = Mnemonic.languages[i]; + wordlist = Mnemonic.getWordlist(lang); + if (wordlist.indexOf(word) !== -1) + return lang; + } + + throw new Error('Could not determine language.'); +}; + +/** + * Retrieve the wordlist for a language. + * @param {String} language + * @returns {String[]} + */ + +Mnemonic.getWordlist = function getWordlist(language) { + switch (language) { + case 'simplified chinese': + return require('../../../etc/chinese-simplified.js'); + case 'traditional chinese': + return require('../../../etc/chinese-traditional.js'); + case 'english': + return require('../../../etc/english.js'); + case 'french': + return require('../../../etc/french.js'); + case 'italian': + return require('../../../etc/italian.js'); + case 'japanese': + return require('../../../etc/japanese.js'); + default: + throw new Error('Unknown language: ' + language); + } +}; + +/** + * Convert mnemonic to a json-friendly object. + * @returns {Object} + */ + +Mnemonic.prototype.toJSON = function toJSON() { + return { + bits: this.bits, + language: this.language, + entropy: this.getEntropy().toString('hex'), + phrase: this.getPhrase(), + passphrase: this.passphrase + }; +}; + +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + +Mnemonic.prototype.fromJSON = function fromJSON(json) { + assert(utils.isNumber(json.bits)); + assert(typeof json.language === 'string'); + assert(typeof json.entropy === 'string'); + assert(typeof json.phrase === 'string'); + assert(typeof json.passphrase === 'string'); + assert(json.bits >= constants.hd.MIN_ENTROPY); + assert(json.bits <= constants.hd.MAX_ENTROPY); + assert(json.bits % 32 === 0); + assert(json.bits / 8 === json.entropy.length / 2); + + this.bits = json.bits; + this.language = json.language; + this.entropy = new Buffer(json.entropy, 'hex'); + this.phrase = json.phrase; + this.passphrase = json.passphrase; + + return this; +}; + +/** + * Instantiate mnemonic from json object. + * @param {Object} json + * @returns {Mnemonic} + */ + +Mnemonic.fromJSON = function fromJSON(json) { + return new Mnemonic().fromJSON(json); +}; + +/** + * Serialize mnemonic. + * @returns {Buffer} + */ + +Mnemonic.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + var lang = Mnemonic.languages.indexOf(this.language); + + assert(lang !== -1); + + p.writeU16(this.bits); + p.writeU8(lang); + p.writeBytes(this.getEntropy()); + p.writeVarString(this.getPhrase(), 'utf8'); + p.writeVarString(this.passphrase, 'utf8'); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +Mnemonic.prototype.fromRaw = function fromRaw(data) { + var p = new BufferReader(data); + + this.bits = p.readU16(); + this.language = Mnemonic.languages[p.readU8()]; + this.entropy = p.readBytes(this.bits / 8); + this.phrase = p.readVarString('utf8'); + this.passphrase = p.readVarString('utf8'); + + assert(this.language); + assert(this.bits >= constants.hd.MIN_ENTROPY); + assert(this.bits <= constants.hd.MAX_ENTROPY); + assert(this.bits % 32 === 0); + + return this; +}; + +/** + * Instantiate mnemonic from serialized data. + * @param {Buffer} data + * @returns {Mnemonic} + */ + +Mnemonic.fromRaw = function fromRaw(data) { + return new Mnemonic().fromRaw(data); +}; + +/** + * Convert the mnemonic to a string. + * @returns {String} + */ + +Mnemonic.prototype.toString = function toString() { + return this.getPhrase(); +}; + +/** + * Inspect the mnemonic. + * @returns {String} + */ + +Mnemonic.prototype.inspect = function inspect() { + return ''; +}; + +/** + * Test whether an object is a Mnemonic. + * @param {Object} obj + * @returns {Boolean} + */ + +Mnemonic.isMnemonic = function isMnemonic(obj) { + return obj + && typeof obj.bits === 'number' + && typeof obj.toSeed === 'function'; +}; + +/* + * Helpers + */ + +function nfkd(str) { + if (str.normalize) + return str.normalize('NFKD'); + + if (!unorm) + unorm = require('../../../vendor/unorm'); + + return unorm.nfkd(str); +} + +/* + * Expose + */ + +module.exports = Mnemonic; diff --git a/lib/bcoin/hd/private.js b/lib/bcoin/hd/private.js new file mode 100644 index 00000000..818c0b31 --- /dev/null +++ b/lib/bcoin/hd/private.js @@ -0,0 +1,794 @@ +/*! + * private.js - hd private keys for bcoin + * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var ec = require('../crypto/ec'); +var assert = utils.assert; +var constants = bcoin.constants; +var networks = bcoin.networks; +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var HD = require('./hd'); + +/* + * Constants + */ + +var FINGER_PRINT = new Buffer('00000000', 'hex'); + +/** + * HDPrivateKey + * @exports HDPrivateKey + * @constructor + * @param {Object|Base58String} options + * @param {Base58String?} options.xkey - Serialized base58 key. + * @param {Mnemonic?} options.mnemonic + * @param {Number?} options.depth + * @param {Buffer?} options.parentFingerPrint + * @param {Number?} options.childIndex + * @param {Buffer?} options.chainCode + * @param {Buffer?} options.privateKey + * @property {Network} network + * @property {Base58String} xprivkey + * @property {Base58String} xpubkey + * @property {Mnemonic?} mnemonic + * @property {Number} depth + * @property {Buffer} parentFingerPrint + * @property {Number} childIndex + * @property {Buffer} chainCode + * @property {Buffer} privateKey + * @property {HDPublicKey} hdPublicKey + */ + +function HDPrivateKey(options) { + if (!(this instanceof HDPrivateKey)) + return new HDPrivateKey(options); + + this.network = bcoin.network.get(); + this.depth = 0; + this.parentFingerPrint = FINGER_PRINT; + this.childIndex = 0; + this.chainCode = constants.ZERO_HASH; + this.privateKey = constants.ZERO_HASH; + + this.publicKey = constants.ZERO_KEY; + this.fingerPrint = null; + + this.mnemonic = null; + + this._xprivkey = null; + + this.hdPrivateKey = this; + this._hdPublicKey = null; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +HDPrivateKey.prototype.fromOptions = function fromOptions(options) { + assert(options, 'No options for HD private key.'); + assert(utils.isNumber(options.depth)); + assert(Buffer.isBuffer(options.parentFingerPrint)); + assert(utils.isNumber(options.childIndex)); + assert(Buffer.isBuffer(options.chainCode)); + assert(Buffer.isBuffer(options.privateKey)); + assert(options.depth <= 0xff, 'Depth is too high.'); + + if (options.network) + this.network = bcoin.network.get(options.network); + + this.depth = options.depth; + this.parentFingerPrint = options.parentFingerPrint; + this.childIndex = options.childIndex; + this.chainCode = options.chainCode; + this.privateKey = options.privateKey; + this.publicKey = ec.publicKeyCreate(options.privateKey, true); + + if (options.mnemonic) { + assert(options.mnemonic instanceof HD.Mnemonic); + this.mnemonic = options.mnemonic; + } + + if (options.xprivkey) { + assert(typeof options.xprivkey === 'string'); + this._xprivkey = options.xprivkey; + } + + return this; +}; + +/** + * Instantiate HD private key from options object. + * @param {Object} options + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromOptions = function fromOptions(options) { + return new HDPrivateKey().fromOptions(options); +}; + +HDPrivateKey.prototype.__defineGetter__('hdPublicKey', function() { + var key = this._hdPublicKey; + + if (!key) { + key = new HD.PublicKey(); + key.network = this.network; + key.depth = this.depth; + key.parentFingerPrint = this.parentFingerPrint; + key.childIndex = this.childIndex; + key.chainCode = this.chainCode; + key.publicKey = this.publicKey; + this._hdPublicKey = key; + } + + return key; +}); + +HDPrivateKey.prototype.__defineGetter__('xprivkey', function() { + if (!this._xprivkey) + this._xprivkey = this.toBase58(); + return this._xprivkey; +}); + +HDPrivateKey.prototype.__defineGetter__('xpubkey', function() { + return this.hdPublicKey.xpubkey; +}); + +/** + * Destroy the key (zeroes chain code, privkey, and pubkey). + * @param {Boolean} pub - Destroy hd public key as well. + */ + +HDPrivateKey.prototype.destroy = function destroy(pub) { + this.depth = 0; + this.parentFingerPrint.fill(0); + this.childIndex = 0; + this.chainCode.fill(0); + this.privateKey.fill(0); + this.publicKey.fill(0); + + if (this.fingerPrint) { + this.fingerPrint.fill(0); + this.fingerPrint = null; + } + + if (this._hdPublicKey) { + if (pub) + this._hdPublicKey.destroy(); + this._hdPublicKey = null; + } + + this._xprivkey = null; + + if (this.mnemonic) { + this.mnemonic.destroy(); + this.mnemonic = null; + } +}; + +/** + * Derive a child key. + * @param {Number|String} - Child index or path. + * @param {Boolean?} hardened - Whether the derivation should be hardened. + * @returns {HDPrivateKey} + */ + +HDPrivateKey.prototype.derive = function derive(index, hardened) { + var p, id, data, hash, left, right, key, child; + + if (typeof index === 'string') + return this.derivePath(index); + + hardened = index >= constants.hd.HARDENED ? true : hardened; + + if (index < constants.hd.HARDENED && hardened) + index += constants.hd.HARDENED; + + if (!(index >= 0 && index < constants.hd.MAX_INDEX)) + throw new Error('Index out of range.'); + + if (this.depth >= 0xff) + throw new Error('Depth too high.'); + + id = this.getID(index); + child = HD.cache.get(id); + + if (child) + return child; + + p = new BufferWriter(); + + if (hardened) { + p.writeU8(0); + p.writeBytes(this.privateKey); + p.writeU32BE(index); + } else { + p.writeBytes(this.publicKey); + p.writeU32BE(index); + } + + data = p.render(); + + hash = utils.hmac('sha512', data, this.chainCode); + left = hash.slice(0, 32); + right = hash.slice(32, 64); + + try { + key = ec.privateKeyTweakAdd(this.privateKey, left); + } catch (e) { + return this.derive(index + 1); + } + + if (!this.fingerPrint) + this.fingerPrint = utils.hash160(this.publicKey).slice(0, 4); + + child = new HDPrivateKey(); + child.network = this.network; + child.depth = this.depth + 1; + child.parentFingerPrint = this.fingerPrint; + child.childIndex = index; + child.chainCode = right; + child.privateKey = key; + child.publicKey = ec.publicKeyCreate(key, true); + + HD.cache.set(id, child); + + return child; +}; + +/** + * Unique HD key ID. + * @private + * @param {Number} index + * @returns {String} + */ + +HDPrivateKey.prototype.getID = function getID(index) { + return this.network.keyPrefix.xprivkey58 + + this.publicKey.toString('hex') + + index; +}; + +/** + * Derive a BIP44 account key. + * @param {Number} accountIndex + * @returns {HDPrivateKey} + * @throws Error if key is not a master key. + */ + +HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) { + assert(utils.isNumber(accountIndex), 'Account index must be a number.'); + assert(this.isMaster(), 'Cannot derive account index.'); + return this + .derive(44, true) + .derive(this.network.type === 'main' ? 0 : 1, true) + .derive(accountIndex, true); +}; + +/** + * Derive a BIP45 purpose key. + * @returns {HDPrivateKey} + */ + +HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45() { + assert(this.isMaster(), 'Cannot derive purpose 45.'); + return this.derive(45, true); +}; + +/** + * Test whether the key is a master key. + * @returns {Boolean} + */ + +HDPrivateKey.prototype.isMaster = function isMaster() { + return this.depth === 0 + && this.childIndex === 0 + && this.parentFingerPrint.readUInt32LE(0, true) === 0; +}; + +/** + * Test whether the key is (most likely) a BIP44 account key. + * @param {Number?} accountIndex + * @returns {Boolean} + */ + +HDPrivateKey.prototype.isAccount44 = function isAccount44(accountIndex) { + if (accountIndex != null) { + if (this.childIndex !== constants.hd.HARDENED + accountIndex) + return false; + } + return this.depth === 3 && this.childIndex >= constants.hd.HARDENED; +}; + +/** + * Test whether the key is a BIP45 purpose key. + * @returns {Boolean} + */ + +HDPrivateKey.prototype.isPurpose45 = function isPurpose45() { + return this.depth === 1 && this.childIndex === constants.hd.HARDENED + 45; +}; + +/** + * Test whether an object is in the form of a base58 xprivkey. + * @param {String} data + * @returns {Boolean} + */ + +HDPrivateKey.isExtended = function isExtended(data) { + var i, type, prefix; + + if (typeof data !== 'string') + return false; + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xprivkey58; + if (data.indexOf(prefix) === 0) + return true; + } + + return false; +}; + +/** + * Test whether a buffer has a valid network prefix. + * @param {Buffer} data + * @returns {NetworkType} + */ + +HDPrivateKey.hasPrefix = function hasPrefix(data) { + var i, version, prefix, type; + + if (!Buffer.isBuffer(data)) + return false; + + version = data.readUInt32BE(0, true); + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xprivkey; + if (version === prefix) + return type; + } + + return false; +}; + +/** + * Test whether a string is a valid path. + * @param {String} path + * @param {Boolean?} hardened + * @returns {Boolean} + */ + +HDPrivateKey.isValidPath = function isValidPath(path) { + if (typeof path !== 'string') + return false; + + try { + HD.parsePath(path, constants.hd.MAX_INDEX); + return true; + } catch (e) { + return false; + } +}; + +/** + * Derive a key from a derivation path. + * @param {String} path + * @returns {HDPrivateKey} + * @throws Error if `path` is not a valid path. + */ + +HDPrivateKey.prototype.derivePath = function derivePath(path) { + var indexes = HD.parsePath(path, constants.hd.MAX_INDEX); + var key = this; + var i; + + for (i = 0; i < indexes.length; i++) + key = key.derive(indexes[i]); + + return key; +}; + +/** + * Compare a key against an object. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPrivateKey.prototype.equal = function equal(obj) { + if (!HDPrivateKey.isHDPrivateKey(obj)) + return false; + + return this.network === obj.network + && this.depth === obj.depth + && utils.equal(this.parentFingerPrint, obj.parentFingerPrint) + && this.childIndex === obj.childIndex + && utils.equal(this.chainCode, obj.chainCode) + && 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 + * @param {Buffer} seed + * @param {(Network|NetworkType)?} network + */ + +HDPrivateKey.prototype.fromSeed = function fromSeed(seed, network) { + var hash, left, right; + + assert(Buffer.isBuffer(seed)); + + if (!(seed.length * 8 >= constants.hd.MIN_ENTROPY + && seed.length * 8 <= constants.hd.MAX_ENTROPY)) { + throw new Error('Entropy not in range.'); + } + + hash = utils.hmac('sha512', seed, 'Bitcoin seed'); + + left = hash.slice(0, 32); + right = hash.slice(32, 64); + + // Only a 1 in 2^127 chance of happening. + if (!ec.privateKeyVerify(left)) + throw new Error('Master private key is invalid.'); + + this.network = bcoin.network.get(network); + this.depth = 0; + this.parentFingerPrint = new Buffer([0, 0, 0, 0]); + this.childIndex = 0; + this.chainCode = right; + this.privateKey = left; + this.publicKey = ec.publicKeyCreate(left, true); + + return this; +}; + +/** + * Instantiate an hd private key from a 512 bit seed. + * @param {Buffer} seed + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromSeed = function fromSeed(seed, network) { + return new HDPrivateKey().fromSeed(seed, network); +}; + +/** + * Inject properties from a mnemonic. + * @private + * @param {Mnemonic|Object} mnemonic + * @param {(Network|NetworkType)?} network + */ + +HDPrivateKey.prototype.fromMnemonic = function fromMnemonic(mnemonic, network) { + if (!(mnemonic instanceof HD.Mnemonic)) + mnemonic = new HD.Mnemonic(mnemonic); + this.fromSeed(mnemonic.toSeed(), network); + this.mnemonic = mnemonic; + return this; +}; + +/** + * Instantiate an hd private key from a mnemonic. + * @param {Mnemonic|Object} mnemonic + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromMnemonic = function fromMnemonic(mnemonic, network) { + return new HDPrivateKey().fromMnemonic(mnemonic, network); +}; + +/** + * Inject properties from privateKey and entropy. + * @private + * @param {Buffer} key + * @param {Buffer} entropy + * @param {(Network|NetworkType)?} network + */ + +HDPrivateKey.prototype.fromKey = function fromKey(key, entropy, network) { + assert(Buffer.isBuffer(key) && key.length === 32); + assert(Buffer.isBuffer(entropy) && entropy.length === 32); + this.network = bcoin.network.get(network); + this.depth = 0; + this.parentFingerPrint = new Buffer([0, 0, 0, 0]); + this.childIndex = 0; + this.chainCode = entropy; + this.privateKey = key; + this.publicKey = ec.publicKeyCreate(key, true); + return this; +}; + +/** + * Create an hd private key from a key and entropy bytes. + * @param {Buffer} key + * @param {Buffer} entropy + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromKey = function fromKey(key, entropy, network) { + return new HDPrivateKey().fromKey(key, entropy, network); +}; + +/** + * Generate an hd private key. + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +HDPrivateKey.generate = function generate(network) { + var key = ec.generatePrivateKey(); + var entropy = ec.random(32); + return HDPrivateKey.fromKey(key, entropy, network); +}; + +/** + * Inject properties from base58 key. + * @private + * @param {Base58String} xkey + */ + +HDPrivateKey.prototype.fromBase58 = function fromBase58(xkey) { + this.fromRaw(utils.fromBase58(xkey)); + this._xprivkey = xkey; + return this; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} raw + */ + +HDPrivateKey.prototype.fromRaw = function fromRaw(raw) { + var p = new BufferReader(raw); + var i, version, type, prefix; + + version = p.readU32BE(); + this.depth = p.readU8(); + this.parentFingerPrint = p.readBytes(4); + this.childIndex = p.readU32BE(); + this.chainCode = p.readBytes(32); + p.readU8(); + this.privateKey = p.readBytes(32); + p.verifyChecksum(); + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xprivkey; + if (version === prefix) + break; + } + + assert(i < networks.types.length, 'Network not found.'); + + this.publicKey = ec.publicKeyCreate(this.privateKey, true); + this.network = bcoin.network.get(type); + + return this; +}; + +/** + * Serialize key to a base58 string. + * @param {(Network|NetworkType)?} network + * @returns {Base58String} + */ + +HDPrivateKey.prototype.toBase58 = function toBase58(network) { + return utils.toBase58(this.toRaw(network)); +}; + +/** + * Serialize the key. + * @param {(Network|NetworkType)?} network + * @returns {Buffer} + */ + +HDPrivateKey.prototype.toRaw = function toRaw(network, writer) { + var p = new BufferWriter(writer); + + if (!network) + network = this.network; + + network = bcoin.network.get(network); + + p.writeU32BE(network.keyPrefix.xprivkey); + p.writeU8(this.depth); + p.writeBytes(this.parentFingerPrint); + p.writeU32BE(this.childIndex); + p.writeBytes(this.chainCode); + p.writeU8(0); + p.writeBytes(this.privateKey); + p.writeChecksum(); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Serialize the key in "extended" + * format (includes the mnemonic). + * @param {(Network|NetworkType)?} network + * @returns {Buffer} + */ + +HDPrivateKey.prototype.toExtended = function toExtended(network, writer) { + var p = new BufferWriter(writer); + + this.toRaw(network, p); + + if (this.mnemonic) { + p.writeU8(1); + this.mnemonic.toRaw(p); + } else { + p.writeU8(0); + } + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from extended serialized data. + * @private + * @param {Buffer} data + */ + +HDPrivateKey.prototype.fromExtended = function fromExtended(data) { + var p = new BufferReader(data); + this.fromRaw(p); + if (p.readU8() === 1) + this.mnemonic = HD.Mnemonic.fromRaw(p); + return this; +}; + +/** + * Instantiate key from "extended" serialized data. + * @param {Buffer} data + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromExtended = function fromExtended(data) { + return new HDPrivateKey().fromExtended(data); +}; + +/** + * Instantiate an HD private key from a base58 string. + * @param {Base58String} xkey + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromBase58 = function fromBase58(xkey) { + return new HDPrivateKey().fromBase58(xkey); +}; + +/** + * Instantiate key from serialized data. + * @param {Buffer} raw + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromRaw = function fromRaw(raw) { + return new HDPrivateKey().fromRaw(raw); +}; + +/** + * Convert key to a more json-friendly object. + * @returns {Object} + */ + +HDPrivateKey.prototype.toJSON = function toJSON() { + return { + xprivkey: this.xprivkey, + mnemonic: this.mnemonic ? this.mnemonic.toJSON() : null + }; +}; + +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + +HDPrivateKey.prototype.fromJSON = function fromJSON(json) { + assert(json.xprivkey, 'Could not handle key JSON.'); + + this.fromBase58(json.xprivkey); + + if (json.mnemonic) + this.mnemonic = HD.Mnemonic.fromJSON(json.mnemonic); + + return this; +}; + +/** + * Instantiate an HDPrivateKey from a jsonified key object. + * @param {Object} json - The jsonified key object. + * @returns {HDPrivateKey} + */ + +HDPrivateKey.fromJSON = function fromJSON(json) { + return new HDPrivateKey().fromJSON(json); +}; + +/** + * Test whether an object is an HDPrivateKey. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPrivateKey.isHDPrivateKey = function isHDPrivateKey(obj) { + return obj + && typeof obj.derive === 'function' + && typeof obj.toExtended === 'function' + && obj.chainCode !== undefined; +}; + +/* + * Expose + */ + +module.exports = HDPrivateKey; diff --git a/lib/bcoin/hd/public.js b/lib/bcoin/hd/public.js new file mode 100644 index 00000000..d0848309 --- /dev/null +++ b/lib/bcoin/hd/public.js @@ -0,0 +1,557 @@ +/*! + * public.js - hd public keys for bcoin + * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var ec = require('../crypto/ec'); +var assert = utils.assert; +var constants = bcoin.constants; +var networks = bcoin.networks; +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var HD = require('./hd'); + +/* + * Constants + */ + +var FINGER_PRINT = new Buffer('00000000', 'hex'); + +/** + * HDPublicKey + * @exports HDPublicKey + * @constructor + * @param {Object|Base58String} options + * @param {Base58String?} options.xkey - Serialized base58 key. + * @param {Number?} options.depth + * @param {Buffer?} options.parentFingerPrint + * @param {Number?} options.childIndex + * @param {Buffer?} options.chainCode + * @param {Buffer?} options.publicKey + * @property {Network} network + * @property {Base58String} xpubkey + * @property {Number} depth + * @property {Buffer} parentFingerPrint + * @property {Number} childIndex + * @property {Buffer} chainCode + * @property {Buffer} publicKey + */ + +function HDPublicKey(options) { + if (!(this instanceof HDPublicKey)) + return new HDPublicKey(options); + + this.network = bcoin.network.get(); + this.depth = 0; + this.parentFingerPrint = FINGER_PRINT; + this.childIndex = 0; + this.chainCode = constants.ZERO_HASH; + this.publicKey = constants.ZERO_KEY; + + this.fingerPrint = null; + + this._xpubkey = null; + + this.hdPublicKey = this; + this.hdPrivateKey = null; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +HDPublicKey.prototype.fromOptions = function fromOptions(options) { + assert(options, 'No options for HDPublicKey'); + assert(utils.isNumber(options.depth)); + assert(Buffer.isBuffer(options.parentFingerPrint)); + assert(utils.isNumber(options.childIndex)); + assert(Buffer.isBuffer(options.chainCode)); + assert(Buffer.isBuffer(options.publicKey)); + + if (options.network) + this.network = bcoin.network.get(options.network); + + this.depth = options.depth; + this.parentFingerPrint = options.parentFingerPrint; + this.childIndex = options.childIndex; + this.chainCode = options.chainCode; + this.publicKey = options.publicKey; + + if (options.xpubkey) { + assert(typeof options.xpubkey === 'string'); + this._xpubkey = options.xpubkey; + } + + return this; +}; + +/** + * Instantiate HD public key from options object. + * @param {Object} options + * @returns {HDPublicKey} + */ + +HDPublicKey.fromOptions = function fromOptions(options) { + return new HDPublicKey().fromOptions(options); +}; + +HDPublicKey.prototype.__defineGetter__('xpubkey', function() { + if (!this._xpubkey) + this._xpubkey = this.toBase58(); + return this._xpubkey; +}); + +/** + * Destroy the key (zeroes chain code and pubkey). + */ + +HDPublicKey.prototype.destroy = function destroy() { + this.depth = 0; + this.parentFingerPrint.fill(0); + this.childIndex = 0; + this.chainCode.fill(0); + this.publicKey.fill(0); + + if (this.fingerPrint) { + this.fingerPrint.fill(0); + this.fingerPrint = null; + } + + this._xpubkey = null; +}; + +/** + * Derive a child key. + * @param {Number|String} - Child index or path. + * @param {Boolean?} hardened - Whether the derivation + * should be hardened (throws if true). + * @returns {HDPrivateKey} + * @throws on `hardened` + */ + +HDPublicKey.prototype.derive = function derive(index, hardened) { + var p, id, data, hash, left, right, key, child; + + if (typeof index === 'string') + return this.derivePath(index); + + if (index >= constants.hd.HARDENED || hardened) + throw new Error('Index out of range.'); + + if (index < 0) + throw new Error('Index out of range.'); + + if (this.depth >= 0xff) + throw new Error('Depth too high.'); + + id = this.getID(index); + child = HD.cache.get(id); + + if (child) + return child; + + p = new BufferWriter(); + p.writeBytes(this.publicKey); + p.writeU32BE(index); + data = p.render(); + + hash = utils.hmac('sha512', data, this.chainCode); + left = hash.slice(0, 32); + right = hash.slice(32, 64); + + try { + key = ec.publicKeyTweakAdd(this.publicKey, left, true); + } catch (e) { + return this.derive(index + 1); + } + + if (!this.fingerPrint) + this.fingerPrint = utils.hash160(this.publicKey).slice(0, 4); + + child = new HDPublicKey(); + child.network = this.network; + child.depth = this.depth + 1; + child.parentFingerPrint = this.fingerPrint; + child.childIndex = index; + child.chainCode = right; + child.publicKey = key; + + HD.cache.set(id, child); + + return child; +}; + +/** + * Unique HD key ID. + * @private + * @param {Number} index + * @returns {String} + */ + +HDPublicKey.prototype.getID = function getID(index) { + return this.network.keyPrefix.xpubkey58 + + this.publicKey.toString('hex') + + index; +}; + +/** + * Derive a BIP44 account key (does not derive, only ensures account key). + * @method + * @param {Number} accountIndex + * @returns {HDPublicKey} + * @throws Error if key is not already an account key. + */ + +HDPublicKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) { + assert(this.isAccount44(accountIndex), 'Cannot derive account index.'); + return this; +}; + +/** + * Derive a BIP45 purpose key (does not derive, only ensures account key). + * @method + * @returns {HDPublicKey} + * @throws Error if key is not already a purpose key. + */ + +HDPublicKey.prototype.derivePurpose45 = function derivePurpose45() { + assert(this.isPurpose45(), 'Cannot derive purpose 45.'); + return this; +}; + +/** + * Test whether the key is a master key. + * @method + * @returns {Boolean} + */ + +HDPublicKey.prototype.isMaster = function() { + return HD.PrivateKey.prototype.isMaster.call(this); +}; + +/** + * Test whether the key is (most likely) a BIP44 account key. + * @method + * @param {Number?} accountIndex + * @returns {Boolean} + */ + +HDPublicKey.prototype.isAccount44 = function(accountIndex) { + return HD.PrivateKey.prototype.isAccount44.call(this, accountIndex); +}; + +/** + * Test whether the key is a BIP45 purpose key. + * @method + * @returns {Boolean} + */ + +HDPublicKey.prototype.isPurpose45 = function() { + return HD.PrivateKey.prototype.isPurpose45.call(this); +}; + +/** + * Test whether a string is a valid path. + * @param {String} path + * @param {Boolean?} hardened + * @returns {Boolean} + */ + +HDPublicKey.isValidPath = function isValidPath(path) { + if (typeof path !== 'string') + return false; + + try { + HD.parsePath(path, constants.hd.HARDENED); + return true; + } catch (e) { + return false; + } +}; + +/** + * Derive a key from a derivation path. + * @param {String} path + * @returns {HDPublicKey} + * @throws Error if `path` is not a valid path. + * @throws Error if hardened. + */ + +HDPublicKey.prototype.derivePath = function derivePath(path) { + var indexes = HD.parsePath(path, constants.hd.HARDENED); + var key = this; + var i; + + for (i = 0; i < indexes.length; i++) + key = key.derive(indexes[i]); + + return key; +}; + +/** + * Compare a key against an object. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPublicKey.prototype.equal = function equal(obj) { + if (!HDPublicKey.isHDPublicKey(obj)) + return false; + + return this.network === obj.network + && this.depth === obj.depth + && utils.equal(this.parentFingerPrint, obj.parentFingerPrint) + && this.childIndex === obj.childIndex + && utils.equal(this.chainCode, obj.chainCode) + && 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} + */ + +HDPublicKey.prototype.toJSON = function toJSON() { + return { + xpubkey: this.xpubkey + }; +}; + +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + +HDPublicKey.prototype.fromJSON = function fromJSON(json) { + assert(json.xpubkey, 'Could not handle HD key JSON.'); + this.fromBase58(json.xpubkey); + return this; +}; + +/** + * Instantiate an HDPrivateKey from a jsonified key object. + * @param {Object} json - The jsonified transaction object. + * @returns {HDPrivateKey} + */ + +HDPublicKey.fromJSON = function fromJSON(json) { + return new HDPublicKey().fromJSON(json); +}; + +/** + * Test whether an object is in the form of a base58 xpubkey. + * @param {String} data + * @returns {Boolean} + */ + +HDPublicKey.isExtended = function isExtended(data) { + var i, type, prefix; + + if (typeof data !== 'string') + return false; + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xpubkey58; + if (data.indexOf(prefix) === 0) + return true; + } + + return false; +}; + +/** + * Test whether a buffer has a valid network prefix. + * @param {Buffer} data + * @returns {NetworkType} + */ + +HDPublicKey.hasPrefix = function hasPrefix(data) { + var i, version, prefix, type; + + if (!Buffer.isBuffer(data)) + return false; + + version = data.readUInt32BE(0, true); + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xpubkey; + if (version === prefix) + return type; + } + + return false; +}; + +/** + * Inject properties from a base58 key. + * @private + * @param {Base58String} xkey + */ + +HDPublicKey.prototype.fromBase58 = function fromBase58(xkey) { + this.fromRaw(utils.fromBase58(xkey)); + this._xpubkey = xkey; + return this; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} raw + */ + +HDPublicKey.prototype.fromRaw = function fromRaw(raw) { + var p = new BufferReader(raw); + var i, version, type, prefix; + + version = p.readU32BE(); + this.depth = p.readU8(); + this.parentFingerPrint = p.readBytes(4); + this.childIndex = p.readU32BE(); + this.chainCode = p.readBytes(32); + this.publicKey = p.readBytes(33); + p.verifyChecksum(); + + for (i = 0; i < networks.types.length; i++) { + type = networks.types[i]; + prefix = networks[type].keyPrefix.xpubkey; + if (version === prefix) + break; + } + + assert(i < networks.types.length, 'Network not found.'); + + this.network = bcoin.network.get(type); + + return this; +}; + +/** + * Serialize key data to base58 extended key. + * @param {Network|String} network + * @returns {Base58String} + */ + +HDPublicKey.prototype.toBase58 = function toBase58(network) { + return utils.toBase58(this.toRaw(network)); +}; + +/** + * Serialize the key. + * @param {Network|NetworkType} network + * @returns {Buffer} + */ + +HDPublicKey.prototype.toRaw = function toRaw(network, writer) { + var p = new BufferWriter(writer); + + if (!network) + network = this.network; + + network = bcoin.network.get(network); + + p.writeU32BE(network.keyPrefix.xpubkey); + p.writeU8(this.depth); + p.writeBytes(this.parentFingerPrint); + p.writeU32BE(this.childIndex); + p.writeBytes(this.chainCode); + p.writeBytes(this.publicKey); + p.writeChecksum(); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Instantiate an HD public key from a base58 string. + * @param {Base58String} xkey + * @returns {HDPublicKey} + */ + +HDPublicKey.fromBase58 = function fromBase58(xkey) { + return new HDPublicKey().fromBase58(xkey); +}; + +/** + * Instantiate key from serialized data. + * @param {Buffer} raw + * @returns {HDPublicKey} + */ + +HDPublicKey.fromRaw = function fromRaw(data) { + return new HDPublicKey().fromRaw(data); +}; + +/** + * Test whether an object is a HDPublicKey. + * @param {Object} obj + * @returns {Boolean} + */ + +HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { + return obj + && typeof obj.derive === 'function' + && typeof obj.toExtended !== 'function' + && obj.chainCode !== undefined; +}; + +/* + * Expose + */ + +module.exports = HDPublicKey; diff --git a/lib/bcoin/net/packets.js b/lib/bcoin/net/packets.js index a391c82b..0b0924b0 100644 --- a/lib/bcoin/net/packets.js +++ b/lib/bcoin/net/packets.js @@ -236,106 +236,6 @@ VersionPacket.fromRaw = function fromRaw(data, enc) { return new VersionPacket().fromRaw(data, enc); }; -/** - * Inv Item - * @param {Number} type - * @param {Hash} hash - * @property {InvType} type - * @property {Hash} hash - */ - -function InvItem(type, hash) { - if (!(this instanceof InvItem)) - return new InvItem(type, hash); - - this.type = type; - this.hash = hash; -} - -/** - * Serialize inv item. - * @returns {Buffer} - */ - -InvItem.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); - - p.writeU32(this.type); - p.writeHash(this.hash); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @param {Buffer} data - */ - -InvItem.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); - this.type = p.readU32(); - this.hash = p.readHash('hex'); - return this; -}; - -/** - * Instantiate inv item from serialized data. - * @param {Buffer} data - * @param {String?} enc - * @returns {InvItem} - */ - -InvItem.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new InvItem().fromRaw(data); -}; - -/** - * Test whether the inv item is a block. - * @returns {Boolean} - */ - -InvItem.prototype.isBlock = function isBlock() { - switch (this.type) { - case constants.inv.BLOCK: - case constants.inv.WITNESS_BLOCK: - case constants.inv.FILTERED_BLOCK: - case constants.inv.WITNESS_FILTERED_BLOCK: - case constants.inv.CMPCT_BLOCK: - return true; - default: - return false; - } -}; - -/** - * Test whether the inv item is a tx. - * @returns {Boolean} - */ - -InvItem.prototype.isTX = function isTX() { - switch (this.type) { - case constants.inv.TX: - case constants.inv.WITNESS_TX: - return true; - default: - return false; - } -}; - -/** - * Test whether the inv item has the witness bit set. - * @returns {Boolean} - */ - -InvItem.prototype.hasWitness = function hasWitness() { - return (this.type & constants.WITNESS_MASK) !== 0; -}; - /** * Represents a `getblocks` packet. * @exports GetBlocksPacket @@ -1102,7 +1002,6 @@ NetworkAddress.prototype.toRaw = function toRaw(full, writer) { */ exports.VersionPacket = VersionPacket; -exports.InvItem = InvItem; exports.GetBlocksPacket = GetBlocksPacket; exports.AlertPacket = AlertPacket; exports.RejectPacket = RejectPacket; diff --git a/lib/bcoin/net/parser.js b/lib/bcoin/net/parser.js index 62dcbc29..82e4dfff 100644 --- a/lib/bcoin/net/parser.js +++ b/lib/bcoin/net/parser.js @@ -568,7 +568,7 @@ Parser.parseInv = function parseInv(p) { assert(count <= 50000, 'Item count too high.'); for (i = 0; i < count; i++) - items.push(bcoin.packets.InvItem.fromRaw(p)); + items.push(bcoin.invitem.fromRaw(p)); return items; }; diff --git a/lib/bcoin/net/peer.js b/lib/bcoin/net/peer.js index a71517ff..ba244185 100644 --- a/lib/bcoin/net/peer.js +++ b/lib/bcoin/net/peer.js @@ -15,7 +15,7 @@ var Parser = require('./parser'); var Framer = require('./framer'); var assert = utils.assert; var constants = bcoin.constants; -var InvItem = bcoin.packets.InvItem; +var InvItem = bcoin.invitem; var VersionPacket = bcoin.packets.VersionPacket; var GetBlocksPacket = bcoin.packets.GetBlocksPacket; var RejectPacket = bcoin.packets.RejectPacket; diff --git a/lib/bcoin/net/pool.js b/lib/bcoin/net/pool.js index 63e90741..26f1f0e5 100644 --- a/lib/bcoin/net/pool.js +++ b/lib/bcoin/net/pool.js @@ -16,7 +16,7 @@ var assert = utils.assert; var constants = bcoin.constants; var VerifyError = bcoin.errors.VerifyError; var NetworkAddress = bcoin.packets.NetworkAddress; -var InvItem = bcoin.packets.InvItem; +var InvItem = bcoin.invitem; /** * A pool of peers for handling all network activity. diff --git a/lib/bcoin/primitives/hd.js b/lib/bcoin/primitives/hd.js deleted file mode 100644 index 6c45695b..00000000 --- a/lib/bcoin/primitives/hd.js +++ /dev/null @@ -1,2230 +0,0 @@ -/*! - * @module hd - * - * @description - * HD mnemonics and keys (BIP32, BIP39) for bcoin. - * Code adapted from bitcore-lib: - * - {@link https://github.com/bitpay/bitcore-lib/blob/master/lib/hdprivatekey.js} - * - {@link https://github.com/bitpay/bitcore-lib/blob/master/lib/hdpublickey.js} - * - {@link https://github.com/ryanxcharles/fullnode/blob/master/lib/bip32.js} - * - {@link https://github.com/bitpay/bitcore-mnemonic/blob/master/lib/mnemonic.js} - * - * @see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki - * @see https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki - * - * @license - * - * BIP32 - * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - * - * Copyright (c) 2013-2015 BitPay, Inc. - * - * Parts of this software are based on Bitcoin Core - * Copyright (c) 2009-2015 The Bitcoin Core developers - * - * Parts of this software are based on fullnode - * Copyright (c) 2014 Ryan X. Charles - * Copyright (c) 2014 reddit, Inc. - * - * Parts of this software are based on BitcoinJS - * Copyright (c) 2011 Stefan Thomas - * - * Parts of this software are based on BitcoinJ - * Copyright (c) 2011 Google Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * BIP39 - * - * The MIT License (MIT) - * - * Copyright (c) 2014 BitPay - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -'use strict'; - -var bcoin = require('../env'); -var utils = require('../utils/utils'); -var ec = require('../crypto/ec'); -var assert = utils.assert; -var constants = bcoin.constants; -var networks = bcoin.networks; -var KeyRing = bcoin.keyring; -var LRU = require('../utils/lru'); -var BufferWriter = require('../utils/writer'); -var BufferReader = require('../utils/reader'); -var unorm; - -/* - * Constants - */ - -var FINGER_PRINT = new Buffer('00000000', 'hex'); - -/** - * HD Mnemonic - * @exports Mnemonic - * @constructor - * @param {Object} options - * @param {Number?} options.bit - Bits of entropy (Must - * be a multiple of 8) (default=128). - * @param {Buffer?} options.entropy - Entropy bytes. Will - * be generated with `options.bits` bits of entropy - * if not present. - * @param {String?} options.phrase - Mnemonic phrase (will - * be generated if not present). - * @param {String?} options.passphrase - Optional salt for - * key stretching (empty string if not present). - * @param {String?} options.language - Language. - */ - -function Mnemonic(options) { - if (!(this instanceof Mnemonic)) - return new Mnemonic(options); - - this.bits = constants.hd.MIN_ENTROPY; - this.language = 'english'; - this.entropy = null; - this.phrase = null; - this.passphrase = ''; - - if (options) - this.fromOptions(options); -} - -/** - * List of languages. - * @const {String[]} - * @default - */ - -Mnemonic.languages = [ - 'simplified chinese', - 'traditional chinese', - 'english', - 'french', - 'italian', - 'japanese' -]; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Mnemonic.prototype.fromOptions = function fromOptions(options) { - if (typeof options === 'string') - options = { phrase: options }; - - if (options.bits != null) { - assert(utils.isNumber(options.bits)); - assert(options.bits >= constants.hd.MIN_ENTROPY); - assert(options.bits <= constants.hd.MAX_ENTROPY); - assert(options.bits % 32 === 0); - this.bits = options.bits; - } - - if (options.language) { - assert(typeof options.language === 'string'); - assert(Mnemonic.languages.indexOf(options.language) !== -1); - this.language = options.language; - } - - if (options.passphrase) { - assert(typeof options.passphrase === 'string'); - this.passphrase = options.passphrase; - } - - if (options.phrase) { - this.fromPhrase(options.phrase); - return this; - } - - if (options.entropy) { - this.fromEntropy(options.entropy); - return this; - } - - return this; -}; - -/** - * Instantiate mnemonic from options. - * @param {Object} options - * @returns {Mnemonic} - */ - -Mnemonic.fromOptions = function fromOptions(options) { - return new Mnemonic().fromOptions(options); -}; - -/** - * Destroy the mnemonic (zeroes entropy). - */ - -Mnemonic.prototype.destroy = function destroy() { - this.bits = constants.hd.MIN_ENTROPY; - this.language = 'english'; - if (this.entropy) { - this.entropy.fill(0); - this.entropy = null; - } - this.phrase = null; - this.passphrase = ''; -}; - -/** - * Generate the seed. - * @param {String?} passphrase - * @returns {Buffer} pbkdf2 seed. - */ - -Mnemonic.prototype.toSeed = function toSeed(passphrase) { - if (!passphrase) - passphrase = this.passphrase; - - this.passphrase = passphrase; - - return utils.pbkdf2Sync( - nfkd(this.getPhrase()), - nfkd('mnemonic' + passphrase), - 2048, 64, 'sha512'); -}; - -/** - * Generate seed and create an hd private key. - * @param {String?} passphrase - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey} - */ - -Mnemonic.prototype.toKey = function toKey(passphrase, network) { - var seed = this.toSeed(passphrase); - var key = HDPrivateKey.fromSeed(seed, network); - key.mnemonic = this; - return key; -}; - -/** - * Get or generate entropy. - * @returns {Buffer} - */ - -Mnemonic.prototype.getEntropy = function getEntropy() { - if (!this.entropy) - this.entropy = ec.random(this.bits / 8); - - assert(this.bits / 8 === this.entropy.length); - - return this.entropy; -}; - -/** - * Generate a mnemonic phrase from chosen language. - * @returns {String} - */ - -Mnemonic.prototype.getPhrase = function getPhrase() { - var i, j, phrase, wordlist, bits, entropy, index, pos, oct, bit; - - if (this.phrase) - return this.phrase; - - phrase = []; - wordlist = Mnemonic.getWordlist(this.language); - - entropy = this.getEntropy(); - bits = this.bits; - - // Append the hash to the entropy to - // make things easy when grabbing - // the checksum bits. - entropy = Buffer.concat([entropy, utils.sha256(entropy)]); - - // Include the first `ENT / 32` bits - // of the hash (the checksum). - bits += bits / 32; - - // Build the mnemonic by reading - // 11 bit indexes from the entropy. - for (i = 0; i < bits / 11; i++) { - index = 0; - for (j = 0; j < 11; j++) { - pos = i * 11 + j; - bit = pos % 8; - oct = (pos - bit) / 8; - index <<= 1; - index |= (entropy[oct] >>> (7 - bit)) & 1; - } - phrase.push(wordlist[index]); - } - - // Japanese likes double-width spaces. - if (this.language === 'japanese') - phrase = phrase.join('\u3000'); - else - phrase = phrase.join(' '); - - this.phrase = phrase; - - return phrase; -}; - -/** - * Inject properties from phrase. - * @private - * @param {String} phrase - */ - -Mnemonic.prototype.fromPhrase = function fromPhrase(phrase) { - var i, j, bits, pos, oct, bit, b, ent, entropy, lang; - var chk, word, wordlist, index, cbits, cbytes, words; - - assert(typeof phrase === 'string'); - - words = phrase.split(/[ \u3000]+/); - bits = words.length * 11; - cbits = bits % 32; - cbytes = Math.ceil(cbits / 8); - bits -= cbits; - - assert(bits >= constants.hd.MIN_ENTROPY); - assert(bits <= constants.hd.MAX_ENTROPY); - assert(bits % 32 === 0); - assert(cbits !== 0, 'Invalid checksum.'); - - ent = new Buffer(Math.ceil((bits + cbits) / 8)); - ent.fill(0); - - lang = Mnemonic.getLanguage(words[0]); - wordlist = Mnemonic.getWordlist(lang); - - for (i = 0; i < words.length; i++) { - word = words[i]; - index = wordlist.indexOf(word); - - if (index === -1) - throw new Error('Could not find word.'); - - for (j = 0; j < 11; j++) { - pos = i * 11 + j; - bit = pos % 8; - oct = (pos - bit) / 8; - b = (index >>> (10 - j)) & 1; - ent[oct] |= b << (7 - bit); - } - } - - entropy = ent.slice(0, ent.length - cbytes); - ent = ent.slice(ent.length - cbytes); - chk = utils.sha256(entropy); - - for (i = 0; i < cbits; i++) { - bit = i % 8; - oct = (i - bit) / 8; - b = (ent[oct] >>> (7 - bit)) & 1; - j = (chk[oct] >>> (7 - bit)) & 1; - if (b !== j) - throw new Error('Invalid checksum.'); - } - - assert(bits / 8 === entropy.length); - - this.bits = bits; - this.language = lang; - this.entropy = entropy; - this.phrase = phrase; - - return this; -}; - -/** - * Instantiate mnemonic from a phrase (validates checksum). - * @param {String} phrase - * @returns {Mnemonic} - * @throws on bad checksum - */ - -Mnemonic.fromPhrase = function fromPhrase(phrase) { - return new Mnemonic().fromPhrase(phrase); -}; - -/** - * Inject properties from entropy. - * @private - * @param {Buffer} entropy - * @param {String?} lang - */ - -Mnemonic.prototype.fromEntropy = function fromEntropy(entropy, lang) { - assert(Buffer.isBuffer(entropy)); - assert(entropy.length * 8 >= constants.hd.MIN_ENTROPY); - assert(entropy.length * 8 <= constants.hd.MAX_ENTROPY); - assert((entropy.length * 8) % 32 === 0); - assert(!lang || Mnemonic.languages.indexOf(lang) !== -1); - - this.entropy = entropy; - this.bits = entropy.length * 8; - - if (lang) - this.language = lang; - - return this; -}; - -/** - * Instantiate mnemonic from entropy. - * @param {Buffer} entropy - * @param {String?} lang - * @returns {Mnemonic} - */ - -Mnemonic.fromEntropy = function fromEntropy(entropy, lang) { - return new Mnemonic().fromEntropy(entropy, lang); -}; - -/** - * Determine a single word's language. - * @param {String} word - * @returns {String} Language. - * @throws on not found. - */ - -Mnemonic.getLanguage = function getLanguage(word) { - var i, lang, wordlist; - - for (i = 0; i < Mnemonic.languages.length; i++) { - lang = Mnemonic.languages[i]; - wordlist = Mnemonic.getWordlist(lang); - if (wordlist.indexOf(word) !== -1) - return lang; - } - - throw new Error('Could not determine language.'); -}; - -/** - * Retrieve the wordlist for a language. - * @param {String} language - * @returns {String[]} - */ - -Mnemonic.getWordlist = function getWordlist(language) { - switch (language) { - case 'simplified chinese': - return require('../../../etc/chinese-simplified.js'); - case 'traditional chinese': - return require('../../../etc/chinese-traditional.js'); - case 'english': - return require('../../../etc/english.js'); - case 'french': - return require('../../../etc/french.js'); - case 'italian': - return require('../../../etc/italian.js'); - case 'japanese': - return require('../../../etc/japanese.js'); - default: - throw new Error('Unknown language: ' + language); - } -}; - -/** - * Convert mnemonic to a json-friendly object. - * @returns {Object} - */ - -Mnemonic.prototype.toJSON = function toJSON() { - return { - bits: this.bits, - language: this.language, - entropy: this.getEntropy().toString('hex'), - phrase: this.getPhrase(), - passphrase: this.passphrase - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -Mnemonic.prototype.fromJSON = function fromJSON(json) { - assert(utils.isNumber(json.bits)); - assert(typeof json.language === 'string'); - assert(typeof json.entropy === 'string'); - assert(typeof json.phrase === 'string'); - assert(typeof json.passphrase === 'string'); - assert(json.bits >= constants.hd.MIN_ENTROPY); - assert(json.bits <= constants.hd.MAX_ENTROPY); - assert(json.bits % 32 === 0); - assert(json.bits / 8 === json.entropy.length / 2); - - this.bits = json.bits; - this.language = json.language; - this.entropy = new Buffer(json.entropy, 'hex'); - this.phrase = json.phrase; - this.passphrase = json.passphrase; - - return this; -}; - -/** - * Instantiate mnemonic from json object. - * @param {Object} json - * @returns {Mnemonic} - */ - -Mnemonic.fromJSON = function fromJSON(json) { - return new Mnemonic().fromJSON(json); -}; - -/** - * Serialize mnemonic. - * @returns {Buffer} - */ - -Mnemonic.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - var lang = Mnemonic.languages.indexOf(this.language); - - assert(lang !== -1); - - p.writeU16(this.bits); - p.writeU8(lang); - p.writeBytes(this.getEntropy()); - p.writeVarString(this.getPhrase(), 'utf8'); - p.writeVarString(this.passphrase, 'utf8'); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Mnemonic.prototype.fromRaw = function fromRaw(data) { - var p = new BufferReader(data); - - this.bits = p.readU16(); - this.language = Mnemonic.languages[p.readU8()]; - this.entropy = p.readBytes(this.bits / 8); - this.phrase = p.readVarString('utf8'); - this.passphrase = p.readVarString('utf8'); - - assert(this.language); - assert(this.bits >= constants.hd.MIN_ENTROPY); - assert(this.bits <= constants.hd.MAX_ENTROPY); - assert(this.bits % 32 === 0); - - return this; -}; - -/** - * Instantiate mnemonic from serialized data. - * @param {Buffer} data - * @returns {Mnemonic} - */ - -Mnemonic.fromRaw = function fromRaw(data) { - return new Mnemonic().fromRaw(data); -}; - -/** - * Convert the mnemonic to a string. - * @returns {String} - */ - -Mnemonic.prototype.toString = function toString() { - return this.getPhrase(); -}; - -/** - * Inspect the mnemonic. - * @returns {String} - */ - -Mnemonic.prototype.inspect = function inspect() { - return ''; -}; - -/** - * Test whether an object is a Mnemonic. - * @param {Object} obj - * @returns {Boolean} - */ - -Mnemonic.isMnemonic = function isMnemonic(obj) { - return obj - && typeof obj.bits === 'number' - && typeof obj.toSeed === 'function'; -}; - -/** - * HD - Abstract class for HD keys. Will - * potentially return an {@link HDPublicKey} - * or {@link HDPrivateKey}. - * @exports HD - * @abstract - * @constructor - * @param {Object} options - {@link HDPublicKey} - * or {@link HDPrivateKey} options. - */ - -function HD(options, network) { - if (!options) - return HD.fromMnemonic(null, network); - return HD.from(options, network); -} - -/** - * Instantiate an HD key (public or private) from an base58 string. - * @param {Base58String} xkey - * @returns {HDPrivateKey|HDPublicKey} - */ - -HD.fromBase58 = function fromBase58(xkey) { - if (HDPrivateKey.isExtended(xkey)) - return HDPrivateKey.fromBase58(xkey); - return HDPublicKey.fromBase58(xkey); -}; - -/** - * Generate an {@link HDPrivateKey}. - * @param {Object} options - * @param {Buffer?} options.privateKey - * @param {Buffer?} options.entropy - * @param {String?} network - * @returns {HDPrivateKey} - */ - -HD.generate = function generate(options, network) { - return HDPrivateKey.generate(options, network); -}; - -/** - * Generate an {@link HDPrivateKey} from a seed. - * @param {Object|Mnemonic|Buffer} options - seed, - * mnemonic, mnemonic options. - * @param {String?} network - * @returns {HDPrivateKey} - */ - -HD.fromSeed = function fromSeed(options, network) { - return HDPrivateKey.fromSeed(options, network); -}; - -/** - * Instantiate an hd private key from a mnemonic. - * @param {Mnemonic|Object} mnemonic - * @param {String?} network - * @returns {HDPrivateKey} - */ - -HD.fromMnemonic = function fromMnemonic(options, network) { - return HDPrivateKey.fromMnemonic(options, network); -}; - -/** - * Instantiate an HD key from a jsonified key object. - * @param {Object} json - The jsonified transaction object. - * @returns {HDPrivateKey|HDPublicKey} - */ - -HD.fromJSON = function fromJSON(json) { - if (json.xprivkey) - return HDPrivateKey.fromJSON(json); - return HDPublicKey.fromJSON(json); -}; - -/** - * Instantiate an HD key from serialized data. - * @param {Buffer} data - * @returns {HDPrivateKey|HDPublicKey} - */ - -HD.fromRaw = function fromRaw(data) { - if (HDPrivateKey.hasPrefix(data)) - return HDPrivateKey.fromRaw(data); - return HDPublicKey.fromRaw(data); -}; - -/** - * Instantiate HD key from extended serialized data. - * @param {Buffer} data - * @returns {HDPrivateKey|HDPublicKey} - */ - -HD.fromExtended = function fromExtended(data) { - if (HDPrivateKey.hasPrefix(data)) - return HDPrivateKey.fromExtended(data); - return HDPublicKey.fromRaw(data); -}; - -/** - * Generate an hdkey from any number of options. - * @param {Object|Mnemonic|Buffer} options - mnemonic, mnemonic - * options, seed, or base58 key. - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey|HDPublicKey} - */ - -HD.from = function from(options, network) { - var xkey; - - assert(options, 'Options required.'); - - if (options.xkey) - xkey = options.xkey; - else if (options.xpubkey) - xkey = options.xpubkey; - else if (options.xprivkey) - xkey = options.xprivkey; - else - xkey = options; - - if (HD.isExtended(xkey)) - return HD.fromBase58(xkey); - - if (HD.hasPrefix(options)) - return HD.fromRaw(options); - - return HD.fromMnemonic(options, network); -}; - -/** - * Test whether an object is in the form of a base58 hd key. - * @param {String} data - * @returns {Boolean} - */ - -HD.isExtended = function isExtended(data) { - return HDPrivateKey.isExtended(data) - || HDPublicKey.isExtended(data); -}; - -/** - * Test whether an object is in the form of a serialized hd key. - * @param {Buffer} data - * @returns {NetworkType} - */ - -HD.hasPrefix = function hasPrefix(data) { - return HDPrivateKey.hasPrefix(data) - || HDPublicKey.hasPrefix(data); -}; - -/** - * Parse a derivation path and return an array of indexes. - * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki - * @param {String} path - * @param {Number?} max - Max index. - * @returns {Number[]} - */ - -HD.parsePath = function parsePath(path, max) { - var parts = path.split('/'); - var root = parts.shift(); - var result = []; - var i, hardened, index; - - if (max == null) - max = constants.hd.MAX_INDEX; - - if (root !== 'm' - && root !== 'M' - && root !== 'm\'' - && root !== 'M\'') { - throw new Error('Bad path root.'); - } - - for (i = 0; i < parts.length; i++) { - index = parts[i]; - hardened = index[index.length - 1] === '\''; - - if (hardened) - index = index.slice(0, -1); - - if (!/^\d+$/.test(index)) - throw new Error('Non-number path index.'); - - index = parseInt(index, 10); - - if (hardened) - index += constants.hd.HARDENED; - - if (!(index >= 0 && index < max)) - throw new Error('Index out of range.'); - - result.push(index); - } - - return result; -}; - -/** - * LRU cache to avoid deriving keys twice. - * @type {LRU} - */ - -HD.cache = new LRU(500); - -/** - * Test whether an object is an HD key. - * @param {Object} obj - * @returns {Boolean} - */ - -HD.isHD = function isHD(obj) { - return HDPrivateKey.isHDPrivateKey(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 - * @constructor - * @param {Object|Base58String} options - * @param {Base58String?} options.xkey - Serialized base58 key. - * @param {Mnemonic?} options.mnemonic - * @param {Number?} options.depth - * @param {Buffer?} options.parentFingerPrint - * @param {Number?} options.childIndex - * @param {Buffer?} options.chainCode - * @param {Buffer?} options.privateKey - * @property {Network} network - * @property {Base58String} xprivkey - * @property {Base58String} xpubkey - * @property {Mnemonic?} mnemonic - * @property {Number} depth - * @property {Buffer} parentFingerPrint - * @property {Number} childIndex - * @property {Buffer} chainCode - * @property {Buffer} privateKey - * @property {HDPublicKey} hdPublicKey - */ - -function HDPrivateKey(options) { - if (!(this instanceof HDPrivateKey)) - return new HDPrivateKey(options); - - this.network = bcoin.network.get(); - this.depth = 0; - this.parentFingerPrint = FINGER_PRINT; - this.childIndex = 0; - this.chainCode = constants.ZERO_HASH; - this.privateKey = constants.ZERO_HASH; - - this.publicKey = constants.ZERO_KEY; - this.fingerPrint = null; - - this.mnemonic = null; - - this._xprivkey = null; - - this.hdPrivateKey = this; - this._hdPublicKey = null; - - if (options) - this.fromOptions(options); -} - -utils.inherits(HDPrivateKey, HD); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -HDPrivateKey.prototype.fromOptions = function fromOptions(options) { - assert(options, 'No options for HD private key.'); - assert(utils.isNumber(options.depth)); - assert(Buffer.isBuffer(options.parentFingerPrint)); - assert(utils.isNumber(options.childIndex)); - assert(Buffer.isBuffer(options.chainCode)); - assert(Buffer.isBuffer(options.privateKey)); - assert(options.depth <= 0xff, 'Depth is too high.'); - - if (options.network) - this.network = bcoin.network.get(options.network); - - this.depth = options.depth; - this.parentFingerPrint = options.parentFingerPrint; - this.childIndex = options.childIndex; - this.chainCode = options.chainCode; - this.privateKey = options.privateKey; - this.publicKey = ec.publicKeyCreate(options.privateKey, true); - - if (options.mnemonic) { - assert(options.mnemonic instanceof Mnemonic); - this.mnemonic = options.mnemonic; - } - - if (options.xprivkey) { - assert(typeof options.xprivkey === 'string'); - this._xprivkey = options.xprivkey; - } - - return this; -}; - -/** - * Instantiate HD private key from options object. - * @param {Object} options - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromOptions = function fromOptions(options) { - return new HDPrivateKey().fromOptions(options); -}; - -HDPrivateKey.prototype.__defineGetter__('hdPublicKey', function() { - var key = this._hdPublicKey; - - if (!key) { - key = new HDPublicKey(); - key.network = this.network; - key.depth = this.depth; - key.parentFingerPrint = this.parentFingerPrint; - key.childIndex = this.childIndex; - key.chainCode = this.chainCode; - key.publicKey = this.publicKey; - this._hdPublicKey = key; - } - - return key; -}); - -HDPrivateKey.prototype.__defineGetter__('xprivkey', function() { - if (!this._xprivkey) - this._xprivkey = this.toBase58(); - return this._xprivkey; -}); - -HDPrivateKey.prototype.__defineGetter__('xpubkey', function() { - return this.hdPublicKey.xpubkey; -}); - -/** - * Destroy the key (zeroes chain code, privkey, and pubkey). - * @param {Boolean} pub - Destroy hd public key as well. - */ - -HDPrivateKey.prototype.destroy = function destroy(pub) { - this.depth = 0; - this.parentFingerPrint.fill(0); - this.childIndex = 0; - this.chainCode.fill(0); - this.privateKey.fill(0); - this.publicKey.fill(0); - - if (this.fingerPrint) { - this.fingerPrint.fill(0); - this.fingerPrint = null; - } - - if (this._hdPublicKey) { - if (pub) - this._hdPublicKey.destroy(); - this._hdPublicKey = null; - } - - this._xprivkey = null; - - if (this.mnemonic) { - this.mnemonic.destroy(); - this.mnemonic = null; - } -}; - -/** - * Derive a child key. - * @param {Number|String} - Child index or path. - * @param {Boolean?} hardened - Whether the derivation should be hardened. - * @returns {HDPrivateKey} - */ - -HDPrivateKey.prototype.derive = function derive(index, hardened) { - var p, id, data, hash, left, right, key, child; - - if (typeof index === 'string') - return this.derivePath(index); - - hardened = index >= constants.hd.HARDENED ? true : hardened; - - if (index < constants.hd.HARDENED && hardened) - index += constants.hd.HARDENED; - - if (!(index >= 0 && index < constants.hd.MAX_INDEX)) - throw new Error('Index out of range.'); - - if (this.depth >= 0xff) - throw new Error('Depth too high.'); - - id = this.getID(index); - child = HD.cache.get(id); - - if (child) - return child; - - p = new BufferWriter(); - - if (hardened) { - p.writeU8(0); - p.writeBytes(this.privateKey); - p.writeU32BE(index); - } else { - p.writeBytes(this.publicKey); - p.writeU32BE(index); - } - - data = p.render(); - - hash = utils.hmac('sha512', data, this.chainCode); - left = hash.slice(0, 32); - right = hash.slice(32, 64); - - try { - key = ec.privateKeyTweakAdd(this.privateKey, left); - } catch (e) { - return this.derive(index + 1); - } - - if (!this.fingerPrint) - this.fingerPrint = utils.hash160(this.publicKey).slice(0, 4); - - child = new HDPrivateKey(); - child.network = this.network; - child.depth = this.depth + 1; - child.parentFingerPrint = this.fingerPrint; - child.childIndex = index; - child.chainCode = right; - child.privateKey = key; - child.publicKey = ec.publicKeyCreate(key, true); - - HD.cache.set(id, child); - - return child; -}; - -/** - * Unique HD key ID. - * @private - * @param {Number} index - * @returns {String} - */ - -HDPrivateKey.prototype.getID = function getID(index) { - return this.network.keyPrefix.xprivkey58 - + this.publicKey.toString('hex') - + index; -}; - -/** - * Derive a BIP44 account key. - * @param {Number} accountIndex - * @returns {HDPrivateKey} - * @throws Error if key is not a master key. - */ - -HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) { - assert(utils.isNumber(accountIndex), 'Account index must be a number.'); - assert(this.isMaster(), 'Cannot derive account index.'); - return this - .derive(44, true) - .derive(this.network.type === 'main' ? 0 : 1, true) - .derive(accountIndex, true); -}; - -/** - * Derive a BIP45 purpose key. - * @returns {HDPrivateKey} - */ - -HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45() { - assert(this.isMaster(), 'Cannot derive purpose 45.'); - return this.derive(45, true); -}; - -/** - * Test whether the key is a master key. - * @returns {Boolean} - */ - -HDPrivateKey.prototype.isMaster = function isMaster() { - return this.depth === 0 - && this.childIndex === 0 - && this.parentFingerPrint.readUInt32LE(0, true) === 0; -}; - -/** - * Test whether the key is (most likely) a BIP44 account key. - * @param {Number?} accountIndex - * @returns {Boolean} - */ - -HDPrivateKey.prototype.isAccount44 = function isAccount44(accountIndex) { - if (accountIndex != null) { - if (this.childIndex !== constants.hd.HARDENED + accountIndex) - return false; - } - return this.depth === 3 && this.childIndex >= constants.hd.HARDENED; -}; - -/** - * Test whether the key is a BIP45 purpose key. - * @returns {Boolean} - */ - -HDPrivateKey.prototype.isPurpose45 = function isPurpose45() { - return this.depth === 1 && this.childIndex === constants.hd.HARDENED + 45; -}; - -/** - * Test whether an object is in the form of a base58 xprivkey. - * @param {String} data - * @returns {Boolean} - */ - -HDPrivateKey.isExtended = function isExtended(data) { - var i, type, prefix; - - if (typeof data !== 'string') - return false; - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xprivkey58; - if (data.indexOf(prefix) === 0) - return true; - } - - return false; -}; - -/** - * Test whether a buffer has a valid network prefix. - * @param {Buffer} data - * @returns {NetworkType} - */ - -HDPrivateKey.hasPrefix = function hasPrefix(data) { - var i, version, prefix, type; - - if (!Buffer.isBuffer(data)) - return false; - - version = data.readUInt32BE(0, true); - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xprivkey; - if (version === prefix) - return type; - } - - return false; -}; - -/** - * Test whether a string is a valid path. - * @param {String} path - * @param {Boolean?} hardened - * @returns {Boolean} - */ - -HDPrivateKey.isValidPath = function isValidPath(path) { - if (typeof path !== 'string') - return false; - - try { - HD.parsePath(path, constants.hd.MAX_INDEX); - return true; - } catch (e) { - return false; - } -}; - -/** - * Derive a key from a derivation path. - * @param {String} path - * @returns {HDPrivateKey} - * @throws Error if `path` is not a valid path. - */ - -HDPrivateKey.prototype.derivePath = function derivePath(path) { - var indexes = HD.parsePath(path, constants.hd.MAX_INDEX); - var key = this; - var i; - - for (i = 0; i < indexes.length; i++) - key = key.derive(indexes[i]); - - return key; -}; - -/** - * Compare a key against an object. - * @param {Object} obj - * @returns {Boolean} - */ - -HDPrivateKey.prototype.equal = function equal(obj) { - if (!HDPrivateKey.isHDPrivateKey(obj)) - return false; - - return this.network === obj.network - && this.depth === obj.depth - && utils.equal(this.parentFingerPrint, obj.parentFingerPrint) - && this.childIndex === obj.childIndex - && utils.equal(this.chainCode, obj.chainCode) - && 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 - * @param {Buffer} seed - * @param {(Network|NetworkType)?} network - */ - -HDPrivateKey.prototype.fromSeed = function fromSeed(seed, network) { - var hash, left, right; - - assert(Buffer.isBuffer(seed)); - - if (!(seed.length * 8 >= constants.hd.MIN_ENTROPY - && seed.length * 8 <= constants.hd.MAX_ENTROPY)) { - throw new Error('Entropy not in range.'); - } - - hash = utils.hmac('sha512', seed, 'Bitcoin seed'); - - left = hash.slice(0, 32); - right = hash.slice(32, 64); - - // Only a 1 in 2^127 chance of happening. - if (!ec.privateKeyVerify(left)) - throw new Error('Master private key is invalid.'); - - this.network = bcoin.network.get(network); - this.depth = 0; - this.parentFingerPrint = new Buffer([0, 0, 0, 0]); - this.childIndex = 0; - this.chainCode = right; - this.privateKey = left; - this.publicKey = ec.publicKeyCreate(left, true); - - return this; -}; - -/** - * Instantiate an hd private key from a 512 bit seed. - * @param {Buffer} seed - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromSeed = function fromSeed(seed, network) { - return new HDPrivateKey().fromSeed(seed, network); -}; - -/** - * Inject properties from a mnemonic. - * @private - * @param {Mnemonic|Object} mnemonic - * @param {(Network|NetworkType)?} network - */ - -HDPrivateKey.prototype.fromMnemonic = function fromMnemonic(mnemonic, network) { - if (!(mnemonic instanceof Mnemonic)) - mnemonic = new Mnemonic(mnemonic); - this.fromSeed(mnemonic.toSeed(), network); - this.mnemonic = mnemonic; - return this; -}; - -/** - * Instantiate an hd private key from a mnemonic. - * @param {Mnemonic|Object} mnemonic - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromMnemonic = function fromMnemonic(mnemonic, network) { - return new HDPrivateKey().fromMnemonic(mnemonic, network); -}; - -/** - * Inject properties from privateKey and entropy. - * @private - * @param {Buffer} key - * @param {Buffer} entropy - * @param {(Network|NetworkType)?} network - */ - -HDPrivateKey.prototype.fromKey = function fromKey(key, entropy, network) { - assert(Buffer.isBuffer(key) && key.length === 32); - assert(Buffer.isBuffer(entropy) && entropy.length === 32); - this.network = bcoin.network.get(network); - this.depth = 0; - this.parentFingerPrint = new Buffer([0, 0, 0, 0]); - this.childIndex = 0; - this.chainCode = entropy; - this.privateKey = key; - this.publicKey = ec.publicKeyCreate(key, true); - return this; -}; - -/** - * Create an hd private key from a key and entropy bytes. - * @param {Buffer} key - * @param {Buffer} entropy - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromKey = function fromKey(key, entropy, network) { - return new HDPrivateKey().fromKey(key, entropy, network); -}; - -/** - * Generate an hd private key. - * @param {(Network|NetworkType)?} network - * @returns {HDPrivateKey} - */ - -HDPrivateKey.generate = function generate(network) { - var key = ec.generatePrivateKey(); - var entropy = ec.random(32); - return HDPrivateKey.fromKey(key, entropy, network); -}; - -/** - * Inject properties from base58 key. - * @private - * @param {Base58String} xkey - */ - -HDPrivateKey.prototype.fromBase58 = function fromBase58(xkey) { - this.fromRaw(utils.fromBase58(xkey)); - this._xprivkey = xkey; - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} raw - */ - -HDPrivateKey.prototype.fromRaw = function fromRaw(raw) { - var p = new BufferReader(raw); - var i, version, type, prefix; - - version = p.readU32BE(); - this.depth = p.readU8(); - this.parentFingerPrint = p.readBytes(4); - this.childIndex = p.readU32BE(); - this.chainCode = p.readBytes(32); - p.readU8(); - this.privateKey = p.readBytes(32); - p.verifyChecksum(); - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xprivkey; - if (version === prefix) - break; - } - - assert(i < networks.types.length, 'Network not found.'); - - this.publicKey = ec.publicKeyCreate(this.privateKey, true); - this.network = bcoin.network.get(type); - - return this; -}; - -/** - * Serialize key to a base58 string. - * @param {(Network|NetworkType)?} network - * @returns {Base58String} - */ - -HDPrivateKey.prototype.toBase58 = function toBase58(network) { - return utils.toBase58(this.toRaw(network)); -}; - -/** - * Serialize the key. - * @param {(Network|NetworkType)?} network - * @returns {Buffer} - */ - -HDPrivateKey.prototype.toRaw = function toRaw(network, writer) { - var p = new BufferWriter(writer); - - if (!network) - network = this.network; - - network = bcoin.network.get(network); - - p.writeU32BE(network.keyPrefix.xprivkey); - p.writeU8(this.depth); - p.writeBytes(this.parentFingerPrint); - p.writeU32BE(this.childIndex); - p.writeBytes(this.chainCode); - p.writeU8(0); - p.writeBytes(this.privateKey); - p.writeChecksum(); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Serialize the key in "extended" - * format (includes the mnemonic). - * @param {(Network|NetworkType)?} network - * @returns {Buffer} - */ - -HDPrivateKey.prototype.toExtended = function toExtended(network, writer) { - var p = new BufferWriter(writer); - - this.toRaw(network, p); - - if (this.mnemonic) { - p.writeU8(1); - this.mnemonic.toRaw(p); - } else { - p.writeU8(0); - } - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from extended serialized data. - * @private - * @param {Buffer} data - */ - -HDPrivateKey.prototype.fromExtended = function fromExtended(data) { - var p = new BufferReader(data); - this.fromRaw(p); - if (p.readU8() === 1) - this.mnemonic = Mnemonic.fromRaw(p); - return this; -}; - -/** - * Instantiate key from "extended" serialized data. - * @param {Buffer} data - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromExtended = function fromExtended(data) { - return new HDPrivateKey().fromExtended(data); -}; - -/** - * Instantiate an HD private key from a base58 string. - * @param {Base58String} xkey - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromBase58 = function fromBase58(xkey) { - return new HDPrivateKey().fromBase58(xkey); -}; - -/** - * Instantiate key from serialized data. - * @param {Buffer} raw - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromRaw = function fromRaw(raw) { - return new HDPrivateKey().fromRaw(raw); -}; - -/** - * Convert key to a more json-friendly object. - * @returns {Object} - */ - -HDPrivateKey.prototype.toJSON = function toJSON() { - return { - xprivkey: this.xprivkey, - mnemonic: this.mnemonic ? this.mnemonic.toJSON() : null - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -HDPrivateKey.prototype.fromJSON = function fromJSON(json) { - assert(json.xprivkey, 'Could not handle key JSON.'); - - this.fromBase58(json.xprivkey); - - if (json.mnemonic) - this.mnemonic = Mnemonic.fromJSON(json.mnemonic); - - return this; -}; - -/** - * Instantiate an HDPrivateKey from a jsonified key object. - * @param {Object} json - The jsonified key object. - * @returns {HDPrivateKey} - */ - -HDPrivateKey.fromJSON = function fromJSON(json) { - return new HDPrivateKey().fromJSON(json); -}; - -/** - * Test whether an object is an HDPrivateKey. - * @param {Object} obj - * @returns {Boolean} - */ - -HDPrivateKey.isHDPrivateKey = function isHDPrivateKey(obj) { - return obj - && typeof obj.derive === 'function' - && typeof obj.toExtended === 'function' - && obj.chainCode !== undefined; -}; - -/** - * HDPublicKey - * @exports HDPublicKey - * @constructor - * @param {Object|Base58String} options - * @param {Base58String?} options.xkey - Serialized base58 key. - * @param {Number?} options.depth - * @param {Buffer?} options.parentFingerPrint - * @param {Number?} options.childIndex - * @param {Buffer?} options.chainCode - * @param {Buffer?} options.publicKey - * @property {Network} network - * @property {Base58String} xpubkey - * @property {Number} depth - * @property {Buffer} parentFingerPrint - * @property {Number} childIndex - * @property {Buffer} chainCode - * @property {Buffer} publicKey - */ - -function HDPublicKey(options) { - if (!(this instanceof HDPublicKey)) - return new HDPublicKey(options); - - this.network = bcoin.network.get(); - this.depth = 0; - this.parentFingerPrint = FINGER_PRINT; - this.childIndex = 0; - this.chainCode = constants.ZERO_HASH; - this.publicKey = constants.ZERO_KEY; - - this.fingerPrint = null; - - this._xpubkey = null; - - this.hdPublicKey = this; - this.hdPrivateKey = null; - - if (options) - this.fromOptions(options); -} - -utils.inherits(HDPublicKey, HD); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -HDPublicKey.prototype.fromOptions = function fromOptions(options) { - assert(options, 'No options for HDPublicKey'); - assert(utils.isNumber(options.depth)); - assert(Buffer.isBuffer(options.parentFingerPrint)); - assert(utils.isNumber(options.childIndex)); - assert(Buffer.isBuffer(options.chainCode)); - assert(Buffer.isBuffer(options.publicKey)); - - if (options.network) - this.network = bcoin.network.get(options.network); - - this.depth = options.depth; - this.parentFingerPrint = options.parentFingerPrint; - this.childIndex = options.childIndex; - this.chainCode = options.chainCode; - this.publicKey = options.publicKey; - - if (options.xpubkey) { - assert(typeof options.xpubkey === 'string'); - this._xpubkey = options.xpubkey; - } - - return this; -}; - -/** - * Instantiate HD public key from options object. - * @param {Object} options - * @returns {HDPublicKey} - */ - -HDPublicKey.fromOptions = function fromOptions(options) { - return new HDPublicKey().fromOptions(options); -}; - -HDPublicKey.prototype.__defineGetter__('xpubkey', function() { - if (!this._xpubkey) - this._xpubkey = this.toBase58(); - return this._xpubkey; -}); - -/** - * Destroy the key (zeroes chain code and pubkey). - */ - -HDPublicKey.prototype.destroy = function destroy() { - this.depth = 0; - this.parentFingerPrint.fill(0); - this.childIndex = 0; - this.chainCode.fill(0); - this.publicKey.fill(0); - - if (this.fingerPrint) { - this.fingerPrint.fill(0); - this.fingerPrint = null; - } - - this._xpubkey = null; -}; - -/** - * Derive a child key. - * @param {Number|String} - Child index or path. - * @param {Boolean?} hardened - Whether the derivation - * should be hardened (throws if true). - * @returns {HDPrivateKey} - * @throws on `hardened` - */ - -HDPublicKey.prototype.derive = function derive(index, hardened) { - var p, id, data, hash, left, right, key, child; - - if (typeof index === 'string') - return this.derivePath(index); - - if (index >= constants.hd.HARDENED || hardened) - throw new Error('Index out of range.'); - - if (index < 0) - throw new Error('Index out of range.'); - - if (this.depth >= 0xff) - throw new Error('Depth too high.'); - - id = this.getID(index); - child = HD.cache.get(id); - - if (child) - return child; - - p = new BufferWriter(); - p.writeBytes(this.publicKey); - p.writeU32BE(index); - data = p.render(); - - hash = utils.hmac('sha512', data, this.chainCode); - left = hash.slice(0, 32); - right = hash.slice(32, 64); - - try { - key = ec.publicKeyTweakAdd(this.publicKey, left, true); - } catch (e) { - return this.derive(index + 1); - } - - if (!this.fingerPrint) - this.fingerPrint = utils.hash160(this.publicKey).slice(0, 4); - - child = new HDPublicKey(); - child.network = this.network; - child.depth = this.depth + 1; - child.parentFingerPrint = this.fingerPrint; - child.childIndex = index; - child.chainCode = right; - child.publicKey = key; - - HD.cache.set(id, child); - - return child; -}; - -/** - * Unique HD key ID. - * @private - * @param {Number} index - * @returns {String} - */ - -HDPublicKey.prototype.getID = function getID(index) { - return this.network.keyPrefix.xpubkey58 - + this.publicKey.toString('hex') - + index; -}; - -/** - * Derive a BIP44 account key (does not derive, only ensures account key). - * @method - * @param {Number} accountIndex - * @returns {HDPublicKey} - * @throws Error if key is not already an account key. - */ - -HDPublicKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) { - assert(this.isAccount44(accountIndex), 'Cannot derive account index.'); - return this; -}; - -/** - * Derive a BIP45 purpose key (does not derive, only ensures account key). - * @method - * @returns {HDPublicKey} - * @throws Error if key is not already a purpose key. - */ - -HDPublicKey.prototype.derivePurpose45 = function derivePurpose45() { - assert(this.isPurpose45(), 'Cannot derive purpose 45.'); - return this; -}; - -/** - * Test whether the key is a master key. - * @method - * @returns {Boolean} - */ - -HDPublicKey.prototype.isMaster = HDPrivateKey.prototype.isMaster; - -/** - * Test whether the key is (most likely) a BIP44 account key. - * @method - * @param {Number?} accountIndex - * @returns {Boolean} - */ - -HDPublicKey.prototype.isAccount44 = HDPrivateKey.prototype.isAccount44; - -/** - * Test whether the key is a BIP45 purpose key. - * @method - * @returns {Boolean} - */ - -HDPublicKey.prototype.isPurpose45 = HDPrivateKey.prototype.isPurpose45; - -/** - * Test whether a string is a valid path. - * @param {String} path - * @param {Boolean?} hardened - * @returns {Boolean} - */ - -HDPublicKey.isValidPath = function isValidPath(path) { - if (typeof path !== 'string') - return false; - - try { - HD.parsePath(path, constants.hd.HARDENED); - return true; - } catch (e) { - return false; - } -}; - -/** - * Derive a key from a derivation path. - * @param {String} path - * @returns {HDPublicKey} - * @throws Error if `path` is not a valid path. - * @throws Error if hardened. - */ - -HDPublicKey.prototype.derivePath = function derivePath(path) { - var indexes = HD.parsePath(path, constants.hd.HARDENED); - var key = this; - var i; - - for (i = 0; i < indexes.length; i++) - key = key.derive(indexes[i]); - - return key; -}; - -/** - * Compare a key against an object. - * @param {Object} obj - * @returns {Boolean} - */ - -HDPublicKey.prototype.equal = function equal(obj) { - if (!HDPublicKey.isHDPublicKey(obj)) - return false; - - return this.network === obj.network - && this.depth === obj.depth - && utils.equal(this.parentFingerPrint, obj.parentFingerPrint) - && this.childIndex === obj.childIndex - && utils.equal(this.chainCode, obj.chainCode) - && 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} - */ - -HDPublicKey.prototype.toJSON = function toJSON() { - return { - xpubkey: this.xpubkey - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -HDPublicKey.prototype.fromJSON = function fromJSON(json) { - assert(json.xpubkey, 'Could not handle HD key JSON.'); - this.fromBase58(json.xpubkey); - return this; -}; - -/** - * Instantiate an HDPrivateKey from a jsonified key object. - * @param {Object} json - The jsonified transaction object. - * @returns {HDPrivateKey} - */ - -HDPublicKey.fromJSON = function fromJSON(json) { - return new HDPublicKey().fromJSON(json); -}; - -/** - * Test whether an object is in the form of a base58 xpubkey. - * @param {String} data - * @returns {Boolean} - */ - -HDPublicKey.isExtended = function isExtended(data) { - var i, type, prefix; - - if (typeof data !== 'string') - return false; - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xpubkey58; - if (data.indexOf(prefix) === 0) - return true; - } - - return false; -}; - -/** - * Test whether a buffer has a valid network prefix. - * @param {Buffer} data - * @returns {NetworkType} - */ - -HDPublicKey.hasPrefix = function hasPrefix(data) { - var i, version, prefix, type; - - if (!Buffer.isBuffer(data)) - return false; - - version = data.readUInt32BE(0, true); - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xpubkey; - if (version === prefix) - return type; - } - - return false; -}; - -/** - * Inject properties from a base58 key. - * @private - * @param {Base58String} xkey - */ - -HDPublicKey.prototype.fromBase58 = function fromBase58(xkey) { - this.fromRaw(utils.fromBase58(xkey)); - this._xpubkey = xkey; - return this; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} raw - */ - -HDPublicKey.prototype.fromRaw = function fromRaw(raw) { - var p = new BufferReader(raw); - var i, version, type, prefix; - - version = p.readU32BE(); - this.depth = p.readU8(); - this.parentFingerPrint = p.readBytes(4); - this.childIndex = p.readU32BE(); - this.chainCode = p.readBytes(32); - this.publicKey = p.readBytes(33); - p.verifyChecksum(); - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - prefix = networks[type].keyPrefix.xpubkey; - if (version === prefix) - break; - } - - assert(i < networks.types.length, 'Network not found.'); - - this.network = bcoin.network.get(type); - - return this; -}; - -/** - * Serialize key data to base58 extended key. - * @param {Network|String} network - * @returns {Base58String} - */ - -HDPublicKey.prototype.toBase58 = function toBase58(network) { - return utils.toBase58(this.toRaw(network)); -}; - -/** - * Serialize the key. - * @param {Network|NetworkType} network - * @returns {Buffer} - */ - -HDPublicKey.prototype.toRaw = function toRaw(network, writer) { - var p = new BufferWriter(writer); - - if (!network) - network = this.network; - - network = bcoin.network.get(network); - - p.writeU32BE(network.keyPrefix.xpubkey); - p.writeU8(this.depth); - p.writeBytes(this.parentFingerPrint); - p.writeU32BE(this.childIndex); - p.writeBytes(this.chainCode); - p.writeBytes(this.publicKey); - p.writeChecksum(); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Instantiate an HD public key from a base58 string. - * @param {Base58String} xkey - * @returns {HDPublicKey} - */ - -HDPublicKey.fromBase58 = function fromBase58(xkey) { - return new HDPublicKey().fromBase58(xkey); -}; - -/** - * Instantiate key from serialized data. - * @param {Buffer} raw - * @returns {HDPublicKey} - */ - -HDPublicKey.fromRaw = function fromRaw(data) { - return new HDPublicKey().fromRaw(data); -}; - -/** - * Test whether an object is a HDPublicKey. - * @param {Object} obj - * @returns {Boolean} - */ - -HDPublicKey.isHDPublicKey = function isHDPublicKey(obj) { - return obj - && typeof obj.derive === 'function' - && typeof obj.toExtended !== 'function' - && obj.chainCode !== undefined; -}; - -[HDPrivateKey, HDPublicKey].forEach(function(HD) { - /** - * Get private key. - * @memberof HDPrivateKey# - * @memberof HDPublicKey# - * @method - * @returns {Buffer} - */ - - HD.prototype.getPrivateKey = KeyRing.prototype.getPrivateKey; - - /** - * Get public key. - * @memberof HDPrivateKey# - * @memberof HDPublicKey# - * @method - * @returns {Buffer} - */ - - HD.prototype.getPublicKey = KeyRing.prototype.getPublicKey; - - /** - * Sign message. - * @memberof HDPrivateKey# - * @memberof HDPublicKey# - * @param {Buffer} msg - * @returns {Buffer} - */ - - HD.prototype.sign = KeyRing.prototype.sign; - - /** - * Verify message. - * @memberof HDPrivateKey# - * @memberof HDPublicKey# - * @param {Buffer} msg - * @param {Buffer} sig - * @returns {Boolean} - */ - - HD.prototype.verify = KeyRing.prototype.verify; - - /** - * Convert HDPrivateKey to a KeyRing. - * @returns {KeyRing} - */ - - HD.prototype.toKeyRing = function toKeyRing() { - return new KeyRing(this.privateKey || this.publicKey, this.network); - }; - - /** - * Whether the key prefers a - * compressed public key. - * Always true. - * @memberof HDPrivateKey# - * @memberof HDPublicKey# - * @type {Boolean} - */ - - HD.prototype.compressed = true; -}); - -/** - * Convert HDPrivateKey to CBitcoinSecret. - * @returns {Base58String} - */ - -HDPrivateKey.prototype.toSecret = function toSecret(network) { - return this.toKeyRing().toSecret(network); -}; - -/* - * Helpers - */ - -function nfkd(str) { - if (str.normalize) - return str.normalize('NFKD'); - - if (!unorm) - unorm = require('../../../vendor/unorm'); - - return unorm.nfkd(str); -} - -/* - * Expose - */ - -exports = HD; -exports.Mnemonic = Mnemonic; -exports.PrivateKey = HDPrivateKey; -exports.PublicKey = HDPublicKey; - -module.exports = HD; diff --git a/lib/bcoin/primitives/input.js b/lib/bcoin/primitives/input.js index ca872d89..a7ca2f52 100644 --- a/lib/bcoin/primitives/input.js +++ b/lib/bcoin/primitives/input.js @@ -11,171 +11,7 @@ var bcoin = require('../env'); var utils = require('../utils/utils'); var assert = utils.assert; var constants = bcoin.constants; - -/** - * Represents a COutPoint. - * @exports Outpoint - * @constructor - * @param {Hash?} hash - * @param {Number?} index - * @property {Hash} hash - * @property {Number} index - */ - -function Outpoint(hash, index) { - if (!(this instanceof Outpoint)) - return new Outpoint(hash, index); - - this.hash = hash || constants.NULL_HASH; - this.index = index != null ? index : 0xffffffff; -} - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Outpoint.prototype.fromOptions = function fromOptions(options) { - assert(typeof options.hash === 'string'); - assert(utils.isNumber(options.index)); - this.hash = options.hash; - this.index = options.index; - return this; -}; - -/** - * Instantate outpoint from options object. - * @param {Object} options - * @returns {Outpoint} - */ - -Outpoint.fromOptions = function fromOptions(options) { - return new Outpoint().fromOptions(options); -}; - -/** - * Test whether the outpoint is null (hash of zeroes - * with max-u32 index). Used to detect coinbases. - * @returns {Boolean} - */ - -Outpoint.prototype.isNull = function isNull() { - return this.index === 0xffffffff && this.hash === constants.NULL_HASH; -}; - -/** - * Serialize outpoint. - * @returns {Buffer} - */ - -Outpoint.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); - - p.writeHash(this.hash); - p.writeU32(this.index); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Outpoint.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); - this.hash = p.readHash('hex'); - this.index = p.readU32(); - return this; -}; - -/** - * Instantiate outpoint from serialized data. - * @param {Buffer} data - * @returns {Outpoint} - */ - -Outpoint.fromRaw = function fromRaw(data) { - return new Outpoint().fromRaw(data); -}; - -/** - * Inject properties from json object. - * @private - * @params {Object} json - */ - -Outpoint.prototype.fromJSON = function fromJSON(json) { - assert(typeof json.hash === 'string'); - assert(utils.isNumber(json.index)); - this.hash = utils.revHex(json.hash); - this.index = json.index; - return this; -}; - -/** - * Convert the outpoint to an object suitable - * for JSON serialization. Note that the hash - * will be reversed to abide by bitcoind's legacy - * of little-endian uint256s. - * @returns {Object} - */ - -Outpoint.prototype.toJSON = function toJSON() { - return { - hash: utils.revHex(this.hash), - index: this.index - }; -}; - -/** - * Instantiate outpoint from json object. - * @param {Object} json - * @returns {Outpoint} - */ - -Outpoint.fromJSON = function fromJSON(json) { - return new Outpoint().fromJSON(json); -}; - -/** - * Inject properties from tx. - * @private - * @param {TX} tx - * @param {Number} index - */ - -Outpoint.prototype.fromTX = function fromTX(tx, index) { - assert(utils.isNumber(index)); - this.hash = tx.hash('hex'); - this.index = index; - return this; -}; - -/** - * Instantiate outpoint from tx. - * @param {TX} tx - * @param {Number} index - * @returns {Outpoint} - */ - -Outpoint.fromTX = function fromTX(tx, index) { - return new Outpoint().fromTX(tx, index); -}; - -/** - * Convert the outpoint to a user-friendly string. - * @returns {String} - */ - -Outpoint.prototype.inspect = function inspect() { - return ''; -}; +var Outpoint = require('./outpoint'); /** * Represents a transaction input. @@ -617,6 +453,4 @@ Input.isInput = function isInput(obj) { * Expose */ -exports = Input; -exports.Outpoint = Outpoint; -module.exports = exports; +module.exports = Input; diff --git a/lib/bcoin/primitives/invitem.js b/lib/bcoin/primitives/invitem.js new file mode 100644 index 00000000..4f8da655 --- /dev/null +++ b/lib/bcoin/primitives/invitem.js @@ -0,0 +1,117 @@ +/*! + * invitem.js - inv item object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var constants = require('../protocol/constants'); + +/** + * Inv Item + * @param {Number} type + * @param {Hash} hash + * @property {InvType} type + * @property {Hash} hash + */ + +function InvItem(type, hash) { + if (!(this instanceof InvItem)) + return new InvItem(type, hash); + + this.type = type; + this.hash = hash; +} + +/** + * Serialize inv item. + * @returns {Buffer} + */ + +InvItem.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.writeU32(this.type); + p.writeHash(this.hash); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @param {Buffer} data + */ + +InvItem.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + this.type = p.readU32(); + this.hash = p.readHash('hex'); + return this; +}; + +/** + * Instantiate inv item from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {InvItem} + */ + +InvItem.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new InvItem().fromRaw(data); +}; + +/** + * Test whether the inv item is a block. + * @returns {Boolean} + */ + +InvItem.prototype.isBlock = function isBlock() { + switch (this.type) { + case constants.inv.BLOCK: + case constants.inv.WITNESS_BLOCK: + case constants.inv.FILTERED_BLOCK: + case constants.inv.WITNESS_FILTERED_BLOCK: + case constants.inv.CMPCT_BLOCK: + return true; + default: + return false; + } +}; + +/** + * Test whether the inv item is a tx. + * @returns {Boolean} + */ + +InvItem.prototype.isTX = function isTX() { + switch (this.type) { + case constants.inv.TX: + case constants.inv.WITNESS_TX: + return true; + default: + return false; + } +}; + +/** + * Test whether the inv item has the witness bit set. + * @returns {Boolean} + */ + +InvItem.prototype.hasWitness = function hasWitness() { + return (this.type & constants.WITNESS_MASK) !== 0; +}; + +/* + * Expose + */ + +module.exports = InvItem; diff --git a/lib/bcoin/primitives/keyring.js b/lib/bcoin/primitives/keyring.js index 6cbbfb62..b56cdc1e 100644 --- a/lib/bcoin/primitives/keyring.js +++ b/lib/bcoin/primitives/keyring.js @@ -882,11 +882,11 @@ function toKey(opt) { if (!opt) return opt; - if (opt.getPrivateKey) - return opt.getPrivateKey(); + if (opt.privateKey) + return opt.privateKey - if (opt.getPublicKey) - return opt.getPublicKey(); + if (opt.publicKey) + return opt.publicKey; return opt; } diff --git a/lib/bcoin/primitives/outpoint.js b/lib/bcoin/primitives/outpoint.js new file mode 100644 index 00000000..34f1f50a --- /dev/null +++ b/lib/bcoin/primitives/outpoint.js @@ -0,0 +1,183 @@ +/*! + * outpoint.js - outpoint object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var assert = utils.assert; +var constants = bcoin.constants; + +/** + * Represents a COutPoint. + * @exports Outpoint + * @constructor + * @param {Hash?} hash + * @param {Number?} index + * @property {Hash} hash + * @property {Number} index + */ + +function Outpoint(hash, index) { + if (!(this instanceof Outpoint)) + return new Outpoint(hash, index); + + this.hash = hash || constants.NULL_HASH; + this.index = index != null ? index : 0xffffffff; +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +Outpoint.prototype.fromOptions = function fromOptions(options) { + assert(typeof options.hash === 'string'); + assert(utils.isNumber(options.index)); + this.hash = options.hash; + this.index = options.index; + return this; +}; + +/** + * Instantate outpoint from options object. + * @param {Object} options + * @returns {Outpoint} + */ + +Outpoint.fromOptions = function fromOptions(options) { + return new Outpoint().fromOptions(options); +}; + +/** + * Test whether the outpoint is null (hash of zeroes + * with max-u32 index). Used to detect coinbases. + * @returns {Boolean} + */ + +Outpoint.prototype.isNull = function isNull() { + return this.index === 0xffffffff && this.hash === constants.NULL_HASH; +}; + +/** + * Serialize outpoint. + * @returns {Buffer} + */ + +Outpoint.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.writeHash(this.hash); + p.writeU32(this.index); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +Outpoint.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + this.hash = p.readHash('hex'); + this.index = p.readU32(); + return this; +}; + +/** + * Instantiate outpoint from serialized data. + * @param {Buffer} data + * @returns {Outpoint} + */ + +Outpoint.fromRaw = function fromRaw(data) { + return new Outpoint().fromRaw(data); +}; + +/** + * Inject properties from json object. + * @private + * @params {Object} json + */ + +Outpoint.prototype.fromJSON = function fromJSON(json) { + assert(typeof json.hash === 'string'); + assert(utils.isNumber(json.index)); + this.hash = utils.revHex(json.hash); + this.index = json.index; + return this; +}; + +/** + * Convert the outpoint to an object suitable + * for JSON serialization. Note that the hash + * will be reversed to abide by bitcoind's legacy + * of little-endian uint256s. + * @returns {Object} + */ + +Outpoint.prototype.toJSON = function toJSON() { + return { + hash: utils.revHex(this.hash), + index: this.index + }; +}; + +/** + * Instantiate outpoint from json object. + * @param {Object} json + * @returns {Outpoint} + */ + +Outpoint.fromJSON = function fromJSON(json) { + return new Outpoint().fromJSON(json); +}; + +/** + * Inject properties from tx. + * @private + * @param {TX} tx + * @param {Number} index + */ + +Outpoint.prototype.fromTX = function fromTX(tx, index) { + assert(utils.isNumber(index)); + this.hash = tx.hash('hex'); + this.index = index; + return this; +}; + +/** + * Instantiate outpoint from tx. + * @param {TX} tx + * @param {Number} index + * @returns {Outpoint} + */ + +Outpoint.fromTX = function fromTX(tx, index) { + return new Outpoint().fromTX(tx, index); +}; + +/** + * Convert the outpoint to a user-friendly string. + * @returns {String} + */ + +Outpoint.prototype.inspect = function inspect() { + return ''; +}; + +/* + * Expose + */ + +module.exports = Outpoint; diff --git a/lib/bcoin/script/opcode.js b/lib/bcoin/script/opcode.js new file mode 100644 index 00000000..b060f2f3 --- /dev/null +++ b/lib/bcoin/script/opcode.js @@ -0,0 +1,176 @@ +/*! + * script.js - script interpreter for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var bn = require('bn.js'); +var constants = bcoin.constants; +var utils = require('../utils/utils'); +var assert = utils.assert; +var opcodes = constants.opcodes; + +/** + * A simple struct which contains + * an opcode and pushdata buffer. + * @exports Opcode + * @constructor + * @param {Number} value - Opcode. + * @param {Buffer?} data - Pushdata buffer. + * @property {Number} value + * @property {Buffer|null} data + */ + +function Opcode(value, data) { + if (!(this instanceof Opcode)) + return new Opcode(value, data); + + this.value = value; + this.data = data || null; +} + +/** + * Encode the opcode. + * @returns {Buffer} + */ + +Opcode.prototype.toRaw = function toRaw(writer) { + return bcoin.script.encode([this], writer); +}; + +/** + * Instantiate an opcode from a number opcode. + * @param {Number} op + * @returns {Opcode} + */ + +Opcode.fromOp = function fromOp(op) { + return new Opcode(op); +}; + +/** + * Instantiate a pushdata opcode from + * a buffer (will encode minimaldata). + * @param {Buffer} data + * @returns {Opcode} + */ + +Opcode.fromData = function fromData(data) { + if (data.length === 0) + return new Opcode(opcodes.OP_0); + + if (data.length === 1) { + if (data[0] >= 1 && data[0] <= 16) + return new Opcode(data[0] + 0x50); + + if (data[0] === 0x81) + return new Opcode(opcodes.OP_1NEGATE); + } + + return Opcode.fromPush(data); +}; + +/** + * Instantiate a pushdata opcode from a + * buffer (this differs from fromData in + * that it will _always_ be a pushdata op). + * @param {Buffer} data + * @returns {Opcode} + */ + +Opcode.fromPush = function fromPush(data) { + if (data.length <= 0x4b) + return new Opcode(data.length, data); + + if (data.length <= 0xff) + return new Opcode(opcodes.OP_PUSHDATA1, data); + + if (data.length <= 0xffff) + return new Opcode(opcodes.OP_PUSHDATA2, data); + + if (data.length <= 0xffffffff) + return new Opcode(opcodes.OP_PUSHDATA4, data); + + throw new Error('Pushdata size too large.'); +}; + +/** + * Instantiate an opcode from a Number. + * @param {Number|BN} num + * @returns {Opcode} + */ + +Opcode.fromNumber = function fromNumber(num) { + return Opcode.fromData(bcoin.script.array(num)); +}; + +/** + * Instantiate an opcode from a small number. + * @param {Number} num + * @returns {Opcode} + */ + +Opcode.fromSmall = function fromSmall(num) { + assert(utils.isNumber(num) && num >= 0 && num <= 16); + return new Opcode(num === 0 ? 0 : num + 0x50); +}; + +/** + * Instantiate a pushdata opcode from a string. + * @param {String} data + * @returns {Opcode} + */ + +Opcode.fromString = function fromString(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + + return Opcode.fromData(data); +}; + +/** + * Instantiate a pushdata opcode from anything. + * @param {String|Buffer|Number|BN|Opcode} data + * @returns {Opcode} + */ + +Opcode.from = function from(data) { + if (data instanceof Opcode) + return data; + + if (typeof data === 'number') + return Opcode.fromOp(data); + + if (Buffer.isBuffer(data)) + return Opcode.fromData(data); + + if (typeof data === 'string') + return Opcode.fromString(data, 'utf8'); + + if (bn.isBN(data)) + return Opcode.fromNumber(data); + + assert(false, 'Bad data for opcode.'); +}; + +/** + * Test whether an object an Opcode. + * @param {Object} obj + * @returns {Boolean} + */ + +Opcode.isOpcode = function isOpcode(obj) { + return obj + && typeof obj.value === 'number' + && (Buffer.isBuffer(obj.data) || obj.data === null); +}; + +/* + * Expose + */ + +module.exports = Opcode; diff --git a/lib/bcoin/script/program.js b/lib/bcoin/script/program.js new file mode 100644 index 00000000..733b5229 --- /dev/null +++ b/lib/bcoin/script/program.js @@ -0,0 +1,109 @@ +/*! + * script.js - script interpreter for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var constants = bcoin.constants; +var utils = require('../utils/utils'); +var assert = utils.assert; +var opcodes = constants.opcodes; +var scriptTypes = constants.scriptTypes; + +/** + * Witness Program + * @constructor + * @param {Number} version + * @param {Buffer} data + * @property {Number} version - Ranges from 0 to 16. + * @property {String|null} type - Null if malformed. `unknown` if unknown + * version (treated as anyone-can-spend). Otherwise one of `witnesspubkeyhash` + * or `witnessscripthash`. + * @property {Buffer} data - The hash (for now). + */ + +function Program(version, data) { + if (!(this instanceof Program)) + return new Program(version, data); + + assert(utils.isNumber(version)); + assert(Buffer.isBuffer(data)); + assert(version >= 0 && version <= 16); + assert(data.length >= 2 && data.length <= 40); + + this.version = version; + this.data = data; +} + +/** + * Get the witness program type. + * @returns {ScriptType} + */ + +Program.prototype.getType = function getType() { + if (this.version === 0) { + if (this.data.length === 20) + return scriptTypes.WITNESSPUBKEYHASH; + + if (this.data.length === 32) + return scriptTypes.WITNESSSCRIPTHASH; + + // Fail on bad version=0 + return scriptTypes.WITNESSMALFORMED; + } + + if (this.version === 1) { + if (this.data.length === 32) + return scriptTypes.WITNESSMASTHASH; + + // Fail on bad version=1 + return scriptTypes.WITNESSMALFORMED; + } + + // No interpretation of script (anyone can spend) + return scriptTypes.NONSTANDARD; +}; + +/** + * Test whether the program is either + * an unknown version or malformed. + * @returns {Boolean} + */ + +Program.prototype.isUnknown = function isUnknown() { + var type = this.getType(); + return type === scriptTypes.WITNESSMALFORMED + || type === scriptTypes.NONSTANDARD; +}; + +/** + * Test whether the program is malformed. + * @returns {Boolean} + */ + +Program.prototype.isMalformed = function isMalformed() { + return this.getType() === scriptTypes.WITNESSMALFORMED; +}; + +/** + * Inspect the program. + * @returns {String} + */ + +Program.prototype.inspect = function inspect() { + return ''; +}; + +/* + * Expose + */ + +module.exports = Program; diff --git a/lib/bcoin/primitives/script.js b/lib/bcoin/script/script.js similarity index 76% rename from lib/bcoin/primitives/script.js rename to lib/bcoin/script/script.js index 24ccd962..f73fbcdf 100644 --- a/lib/bcoin/primitives/script.js +++ b/lib/bcoin/script/script.js @@ -20,1111 +20,9 @@ var STACK_FALSE = new Buffer([]); var STACK_NEGATE = new Buffer([0x81]); var ScriptError = bcoin.errors.ScriptError; var scriptTypes = constants.scriptTypes; - -/** - * Refers to the witness field of segregated witness transactions. - * @exports Witness - * @constructor - * @param {Buffer[]|NakedWitness} items - Array of - * stack items. - * @property {Buffer[]} items - * @property {Script?} redeem - * @property {Number} length - */ - -function Witness(options) { - if (!(this instanceof Witness)) - return new Witness(options); - - this.items = []; - - if (options) - this.fromOptions(options); -} - -Witness.prototype.__defineGetter__('length', function() { - return this.items.length; -}); - -Witness.prototype.__defineSetter__('length', function(length) { - return this.items.length = length; -}); - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Witness.prototype.fromOptions = function fromOptions(options) { - var items; - - assert(options, 'Witness data is required.'); - - items = options.items; - - if (!items) - items = options; - - if (items) - this.fromArray(items); - - return this; -}; - -/** - * Instantiate witness from options. - * @param {Object} options - * @returns {Witness} - */ - -Witness.fromOptions = function fromOptions(options) { - return new Witness().fromOptions(options); -}; - -/** - * Convert witness to an array of buffers. - * @returns {Buffer[]} - */ - -Witness.prototype.toArray = function toArray() { - return this.items.slice(); -}; - -/** - * Inject properties from an array of buffers. - * @private - * @param {Buffer[]} items - */ - -Witness.prototype.fromArray = function fromArray(items) { - assert(Array.isArray(items)); - this.items = items; - return this; -}; - -/** - * Insantiate witness from an array of buffers. - * @param {Buffer[]} items - * @returns {Witness} - */ - -Witness.fromArray = function fromArray(items) { - return new Witness().fromArray(items); -}; - -/** - * Inspect a Witness object. - * @returns {String} Human-readable script. - */ - -Witness.prototype.inspect = function inspect() { - return ''; -}; - -/** - * Convert a Witness object to a String. - * @returns {String} Human-readable script. - */ - -Witness.prototype.toString = function toString() { - return Witness.format(this.items); -}; - -/** - * Format the witness object as bitcoind asm. - * @param {Boolean?} decode - Attempt to decode hash types. - * @returns {String} Human-readable script. - */ - -Witness.prototype.toASM = function toASM(decode) { - return Script.formatASM(Script.parseArray(this.items), decode); -}; - -/** - * Clone the witness object. Note that the raw - * encoded witness data will be lost. This is - * because the function assumes you are going - * to be altering the stack items by hand. - * @returns {Witness} A clone of the current witness object. - */ - -Witness.prototype.clone = function clone() { - return new Witness(this.items.slice()); -}; - -/** - * Convert the Witness to a Stack object. - * This is usually done before executing - * a witness program. - * @returns {Stack} - */ - -Witness.prototype.toStack = function toStack() { - return new Stack(this.items.slice()); -}; - -/** - * "Guess" the type of the witness. - * This method is not 100% reliable. - * @returns {ScriptType} - */ - -Witness.prototype.getInputType = function getInputType() { - if (this.isPubkeyhashInput()) - return scriptTypes.WITNESSPUBKEYHASH; - - if (this.isScripthashInput()) - return scriptTypes.WITNESSSCRIPTHASH; - - return scriptTypes.NONSTANDARD; -}; - -/** - * "Guess" the address of the witness. - * This method is not 100% reliable. - * @returns {Address|null} - */ - -Witness.prototype.getInputAddress = function getInputAddress() { - return bcoin.address.fromWitness(this); -}; - -/** - * "Test" whether the witness is a pubkey input. - * Always returns false. - * @returns {Boolean} - */ - -Witness.prototype.isPubkeyInput = function isPubkeyInput() { - return false; -}; - -/** - * "Guess" whether the witness is a pubkeyhash input. - * This method is not 100% reliable. - * @returns {Boolean} - */ - -Witness.prototype.isPubkeyhashInput = function isPubkeyhashInput() { - return this.items.length === 2 - && Script.isSignatureEncoding(this.items[0]) - && Script.isKeyEncoding(this.items[1]); -}; - -/** - * "Test" whether the witness is a multisig input. - * Always returns false. - * @returns {Boolean} - */ - -Witness.prototype.isMultisigInput = function isMultisigInput() { - return false; -}; - -/** - * "Guess" whether the witness is a scripthash input. - * This method is not 100% reliable. - * @returns {Boolean} - */ - -Witness.prototype.isScripthashInput = function isScripthashInput() { - return this.items.length > 0 && !this.isPubkeyhashInput(); -}; - -/** - * "Guess" whether the witness is an unknown/non-standard type. - * This method is not 100% reliable. - * @returns {Boolean} - */ - -Witness.prototype.isUnknownInput = function isUnknownInput() { - return this.getInputType() === scriptTypes.NONSTANDARD; -}; - -/** - * Test the witness against a bloom filter. - * @param {Bloom} filter - * @returns {Boolean} - */ - -Witness.prototype.test = function test(filter) { - var i, chunk; - - for (i = 0; i < this.items.length; i++) { - chunk = this.items[i]; - - if (chunk.length === 0) - continue; - - if (filter.test(chunk)) - return true; - } - - return false; -}; - -/** - * Grab and deserialize the redeem script from the witness. - * @returns {Script} Redeem script. - */ - -Witness.prototype.getRedeem = function getRedeem() { - var redeem = this.items[this.items.length - 1]; - - if (!redeem) - return; - - return new Script(redeem); -}; - -/** - * Does nothing currently. - */ - -Witness.prototype.compile = function compile() { - // NOP -}; - -/** - * Find a data element in a witness. - * @param {Buffer} data - Data element to match against. - * @returns {Number} Index (`-1` if not present). - */ - -Witness.prototype.indexOf = function indexOf(data) { - return utils.indexOf(this.items, data); -}; - -/** - * Encode the witness to a Buffer. - * @param {String} enc - Encoding, either `'hex'` or `null`. - * @returns {Buffer|String} Serialized script. - */ - -Witness.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); - var i; - - p.writeVarint(this.items.length); - - for (i = 0; i < this.items.length; i++) - p.writeVarBytes(this.items[i]); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Convert witness to a hex string. - * @returns {String} - */ - -Witness.prototype.toJSON = function toJSON() { - return this.toRaw().toString('hex'); -}; - -/** - * Inject properties from json object. - * @private - * @param {String} json - */ - -Witness.prototype.fromJSON = function fromJSON(json) { - assert(typeof json === 'string'); - return this.fromRaw(new Buffer(json, 'hex')); -}; - -/** - * Insantiate witness from a hex string. - * @param {String} json - * @returns {Witness} - */ - -Witness.fromJSON = function fromJSON(json) { - return new Witness().fromJSON(json); -}; - -/** - * Unshift an item onto the witness vector. - * @param {Number|String|Buffer|BN} data - * @returns {Number} - */ - -Witness.prototype.unshift = function unshift(data) { - return this.items.unshift(Witness.encodeItem(data)); -}; - -/** - * Push an item onto the witness vector. - * @param {Number|String|Buffer|BN} data - * @returns {Number} - */ - -Witness.prototype.push = function push(data) { - return this.items.push(Witness.encodeItem(data)); -}; - -/** - * Shift an item off the witness vector. - * @returns {Buffer} - */ - -Witness.prototype.shift = function shift() { - return this.items.shift(); -}; - -/** - * Shift an item off the witness vector. - * @returns {Buffer} - */ - -Witness.prototype.pop = function push(data) { - return this.items.pop(); -}; - -/** - * Remove an item from the witness vector. - * @param {Number} index - * @returns {Buffer} - */ - -Witness.prototype.remove = function remove(i) { - return this.items.splice(i, 1)[0]; -}; - -/** - * Insert an item into the witness vector. - * @param {Number} index - * @param {Number|String|Buffer|BN} data - */ - -Witness.prototype.insert = function insert(i, data) { - assert(i <= this.items.length, 'Index out of bounds.'); - this.items.splice(i, 0, Witness.encodeItem(data))[0]; -}; - -/** - * Get an item from the witness vector. - * @param {Number} index - * @returns {Buffer} - */ - -Witness.prototype.get = function get(i) { - return this.items[i]; -}; - -/** - * Get a small int (0-16) from the witness vector. - * @param {Number} index - * @returns {Number} `-1` on non-existent. - */ - -Witness.prototype.getSmall = function getSmall(i) { - var item = this.items[i]; - if (!item || item.length > 1) - return -1; - if (item.length === 0) - return 0; - if (!(item[0] >= 1 && item[1] <= 16)) - return -1; - return item[0]; -}; - -/** - * Get a number from the witness vector. - * @param {Number} index - * @returns {BN} - */ - -Witness.prototype.getNumber = function getNumber(i) { - var item = this.items[i]; - if (!item || item.length > 5) - return; - return Script.num(item, constants.flags.VERIFY_NONE, 5); -}; - -/** - * Get a string from the witness vector. - * @param {Number} index - * @returns {String} - */ - -Witness.prototype.getString = function getString(i) { - var item = this.items[i]; - if (!item) - return; - return item.toString('utf8'); -}; - -/** - * Clear the witness items. - */ - -Witness.prototype.clear = function clear() { - this.items.length = 0; -}; - -/** - * Set an item in the witness vector. - * @param {Number} index - * @param {Number|String|Buffer|BN} data - */ - -Witness.prototype.set = function set(i, data) { - assert(i <= this.items.length, 'Index out of bounds.'); - this.items[i] = Witness.encodeItem(data); -}; - -/** - * Encode a witness item. - * @param {Number|String|Buffer|BN} data - * @returns {Buffer} - */ - -Witness.encodeItem = function encodeItem(data) { - if (data instanceof Opcode) - data = data.data || data.value; - - if (typeof data === 'number') { - if (data === opcodes.OP_1NEGATE) - return STACK_NEGATE; - - if (data === opcodes.OP_0) - return STACK_FALSE; - - if (data >= opcodes.OP_1 && data <= opcodes.OP_16) - return new Buffer([data - 0x50]); - - throw new Error('Non-push opcode in witness.'); - } - - if (bn.isBN(data)) - return Script.array(data); - - if (typeof data === 'string') - return new Buffer(data, 'utf8'); - - return data; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Witness.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); - var chunkCount = p.readVarint(); - var i; - - for (i = 0; i < chunkCount; i++) - this.items.push(p.readVarBytes()); - - return this; -}; - -/** - * Create a Witness from a serialized buffer. - * @param {Buffer|String} data - Serialized witness. - * @param {String?} enc - Either `"hex"` or `null`. - * @returns {Witness} - */ - -Witness.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new Witness().fromRaw(data); -}; - -/** - * Inject items from string. - * @private - * @param {String|String[]} items - */ - -Witness.prototype.fromString = function fromString(items) { - var i; - - if (!Array.isArray(items)) { - assert(typeof items === 'string'); - - items = items.trim(); - - if (items.length === 0) - return this; - - items = items.split(/\s+/); - } - - for (i = 0; i < items.length; i++) - this.items.push(new Buffer(items[i], 'hex')); - - return this; -}; - -/** - * Parse a test script/array - * string into a witness object. _Must_ - * contain only stack items (no non-push - * opcodes). - * @param {String|String[]} items - Script string. - * @returns {Witness} - * @throws Parse error. - */ - -Witness.fromString = function fromString(items) { - return new Witness().fromString(items); -}; - -/** - * Format script code into a human readable-string. - * @param {Array} code - * @returns {String} Human-readable string. - */ - -Witness.format = function format(items) { - var out = []; - var i; - - for (i = 0; i < items.length; i++) - out.push(items[i].toString('hex')); - - return out.join(' '); -}; - -/** - * Test an object to see if it is a Witness. - * @param {Object} obj - * @returns {Boolean} - */ - -Witness.isWitness = function isWitness(obj) { - return obj - && Array.isArray(obj.items) - && typeof obj.toStack === 'function'; -}; - -/** - * Represents the stack of a Script during execution. - * @exports Stack - * @constructor - * @param {Buffer[]?} items - Stack items. - * @property {Buffer[]} items - Stack items. - * @property {Number} length - Size of stack. - */ - -function Stack(items) { - if (!(this instanceof Stack)) - return new Stack(items); - - this.items = items || []; -} - -Stack.prototype.__defineGetter__('length', function() { - return this.items.length; -}); - -Stack.prototype.__defineSetter__('length', function(length) { - return this.items.length = length; -}); - -/** - * Inspect the stack. - * @returns {String} Human-readable stack. - */ - -Stack.prototype.inspect = function inspect() { - return ''; -}; - -/** - * Convert the stack to a string. - * @returns {String} Human-readable stack. - */ - -Stack.prototype.toString = function toString() { - return Witness.format(this.items); -}; - -/** - * Format the stack as bitcoind asm. - * @param {Boolean?} decode - Attempt to decode hash types. - * @returns {String} Human-readable script. - */ - -Stack.prototype.toASM = function toASM(decode) { - return Script.formatASM(this.items, decode); -}; - -/** - * Pop the redeem script off the stack and deserialize it. - * @returns {Script|null} The redeem script. - */ - -Stack.prototype.getRedeem = function getRedeem() { - var redeem = this.items[this.items.length - 1]; - if (!redeem) - return; - return new Script(redeem); -}; - -/** - * Clone the stack. - * @returns {Stack} Cloned stack. - */ - -Stack.prototype.clone = function clone() { - return new Stack(this.items.slice()); -}; - -/** - * Get total size of the stack, including the alt stack. - * @param {Array} alt - Alt stack. - * @returns {Number} - */ - -Stack.prototype.getSize = function getSize(alt) { - return this.items.length + alt.length; -}; - -/** - * Push item onto stack. - * @see Array#push - * @param {Buffer} item - * @returns {Number} Stack size. - */ - -Stack.prototype.push = function push(item) { - return this.items.push(item); -}; - -/** - * Unshift item from stack. - * @see Array#unshift - * @param {Buffer} item - * @returns {Number} - */ - -Stack.prototype.unshift = function unshift(item) { - return this.items.unshift(item); -}; - -/** - * Slice out part of the stack items. - * @param {Number} start - * @param {Number} end - * @see Array#slice - * @returns {Buffer[]} - */ - -Stack.prototype.slice = function slice(start, end) { - return this.items.slice(start, end); -}; - -/** - * Splice stack items. - * @see Array#splice - * @param {Number} index - * @param {Number} remove - * @param {Buffer?} insert - * @returns {Buffer[]} - */ - -Stack.prototype.splice = function splice(i, remove, insert) { - if (insert === undefined) - return this.items.splice(i, remove); - return this.items.splice(i, remove, insert); -}; - -/** - * Pop a stack item. - * @see Array#pop - * @returns {Buffer|null} - */ - -Stack.prototype.pop = function pop() { - return this.items.pop(); -}; - -/** - * Shift a stack item. - * @see Array#shift - * @returns {Buffer|null} - */ - -Stack.prototype.shift = function shift() { - return this.items.shift(); -}; - -/** - * Get a stack item by index. - * @param {Number} index - * @returns {Buffer|null} - */ - -Stack.prototype.get = function get(i) { - return this.items[i]; -}; - -/** - * Get a stack item relative to - * the top of the stack. - * @example - * stack.top(-1); - * @param {Number} index - * @returns {Buffer|null} - */ - -Stack.prototype.top = function top(i) { - return this.items[this.items.length + i]; -}; - -/** - * Clear the stack. - */ - -Stack.prototype.clear = function clear() { - this.items.length = 0; -}; - -/** - * Set stack item at index. - * @param {Number} index - * @param {Buffer} value - * @returns {Buffer} - */ - -Stack.prototype.set = function set(i, value) { - return this.items[i] = value; -}; - -/** - * Swap stack values. - * @private - * @param {Number} i1 - Index 1. - * @param {Number} i2 - Index 2. - */ - -Stack.prototype._swap = function _swap(i1, i2) { - var v1, v2; - - i1 = this.items.length + i1; - i2 = this.items.length + i2; - - v1 = this.items[i1]; - v2 = this.items[i2]; - - this.items[i1] = v2; - this.items[i2] = v1; -}; - -/** - * Perform the OP_TOALTSTACK operation. - * @param {Array} alt - Alt stack. - * @throws {ScriptError} - */ - -Stack.prototype.toalt = function toalt(alt) { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TOALTSTACK); - - alt.push(this.pop()); -}; - -/** - * Perform the OP_FROMALTSTACK operation. - * @param {Array} alt - Alt stack. - * @throws {ScriptError} - */ - -Stack.prototype.fromalt = function fromalt(alt) { - if (alt.length === 0) - throw new ScriptError('INVALID_ALTSTACK_OPERATION', opcodes.OP_FROMALTSTACK); - - this.push(alt.pop()); -}; - -/** - * Perform the OP_IFDUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.ifdup = function ifdup() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_IFDUP); - - if (Script.bool(this.top(-1))) - this.push(this.top(-1)); -}; - -/** - * Perform the OP_DEPTH operation. - * @throws {ScriptError} - */ - -Stack.prototype.depth = function depth() { - this.push(Script.array(this.length)); -}; - -/** - * Perform the OP_DROP operation. - * @throws {ScriptError} - */ - -Stack.prototype.drop = function drop() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DROP); - - this.pop(); -}; - -/** - * Perform the OP_DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup = function dup() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DUP); - - this.push(this.top(-1)); -}; - -/** - * Perform the OP_NIP operation. - * @throws {ScriptError} - */ - -Stack.prototype.nip = function nip() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_NIP); - - this.splice(this.length - 2, 1); -}; - -/** - * Perform the OP_OVER operation. - * @throws {ScriptError} - */ - -Stack.prototype.over = function over() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_OVER); - - this.push(this.top(-2)); -}; - -/** - * Perform the OP_PICK operation. - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype.pick = function pick(flags) { - return this._pickroll(opcodes.OP_PICK, flags); -}; - -/** - * Perform the OP_ROLL operation. - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype.roll = function roll(flags) { - return this._pickroll(opcodes.OP_ROLL, flags); -}; - -/** - * Perform a pick or roll. - * @private - * @param {Number} op - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype._pickroll = function pickroll(op, flags) { - var val, n; - - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', op); - - val = this.pop(); - n = Script.num(val, flags).toNumber(); - - if (n < 0 || n >= this.length) - throw new ScriptError('INVALID_STACK_OPERATION', op); - - val = this.top(-n - 1); - - if (op === opcodes.OP_ROLL) - this.splice(this.length - n - 1, 1); - - this.push(val); -}; - -/** - * Perform the OP_ROT operation. - * @throws {ScriptError} - */ - -Stack.prototype.rot = function rot() { - if (this.length < 3) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_ROT); - - this._swap(-3, -2); - this._swap(-2, -1); -}; - -/** - * Perform the OP_SWAP operation. - * @throws {ScriptError} - */ - -Stack.prototype.swap = function swap() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SWAP); - - this._swap(-2, -1); -}; - -/** - * Perform the OP_TUCK operation. - * @throws {ScriptError} - */ - -Stack.prototype.tuck = function tuck() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TUCK); - - this.splice(this.length - 2, 0, this.top(-1)); -}; - -/** - * Perform the OP_2DROP operation. - * @throws {ScriptError} - */ - -Stack.prototype.drop2 = function drop2() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DROP); - - this.pop(); - this.pop(); -}; - -/** - * Perform the OP_2DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup2 = function dup2() { - var v1, v2; - - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DUP); - - v1 = this.top(-2); - v2 = this.top(-1); - - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_3DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup3 = function dup3() { - var v1, v2, v3; - - if (this.length < 3) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_3DUP); - - v1 = this.top(-3); - v2 = this.top(-2); - v3 = this.top(-1); - - this.push(v1); - this.push(v2); - this.push(v3); -}; - -/** - * Perform the OP_2OVER operation. - * @throws {ScriptError} - */ - -Stack.prototype.over2 = function over2() { - var v1, v2; - - if (this.length < 4) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2OVER); - - v1 = this.top(-4); - v2 = this.top(-3); - - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_2ROT operation. - * @throws {ScriptError} - */ - -Stack.prototype.rot2 = function rot2() { - var v1, v2; - - if (this.length < 6) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2ROT); - - v1 = this.top(-6); - v2 = this.top(-5); - - this.splice(this.length - 6, 2); - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_2SWAP operation. - * @throws {ScriptError} - */ - -Stack.prototype.swap2 = function swap2() { - if (this.length < 4) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2SWAP); - - this._swap(-4, -2); - this._swap(-3, -1); -}; - -/** - * Perform the OP_SIZE operation. - * @throws {ScriptError} - */ - -Stack.prototype.size = function size() { - if (this.length < 1) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SIZE); - - this.push(Script.array(this.top(-1).length)); -}; - -/** - * Test an object to see if it is a Stack. - * @param {Object} obj - * @returns {Boolean} - */ - -Stack.isStack = function isStack(obj) { - return obj && Array.isArray(obj.items) && typeof obj.swap2 === 'function'; -}; +var Program = require('./program'); +var Opcode = require('./opcode'); +var Stack = require('./stack'); /** * Represents a input or output script. @@ -4709,249 +3607,6 @@ Script.isScript = function isScript(obj) { && typeof obj.getSubscript === 'function'; }; -/** - * A simple struct which contains - * an opcode and pushdata buffer. - * @exports Opcode - * @constructor - * @param {Number} value - Opcode. - * @param {Buffer?} data - Pushdata buffer. - * @property {Number} value - * @property {Buffer|null} data - */ - -function Opcode(value, data) { - if (!(this instanceof Opcode)) - return new Opcode(value, data); - - this.value = value; - this.data = data || null; -} - -/** - * Encode the opcode. - * @returns {Buffer} - */ - -Opcode.prototype.toRaw = function toRaw(writer) { - return Script.encode([this], writer); -}; - -/** - * Instantiate an opcode from a number opcode. - * @param {Number} op - * @returns {Opcode} - */ - -Opcode.fromOp = function fromOp(op) { - return new Opcode(op); -}; - -/** - * Instantiate a pushdata opcode from - * a buffer (will encode minimaldata). - * @param {Buffer} data - * @returns {Opcode} - */ - -Opcode.fromData = function fromData(data) { - if (data.length === 0) - return new Opcode(opcodes.OP_0); - - if (data.length === 1) { - if (data[0] >= 1 && data[0] <= 16) - return new Opcode(data[0] + 0x50); - - if (data[0] === 0x81) - return new Opcode(opcodes.OP_1NEGATE); - } - - return Opcode.fromPush(data); -}; - -/** - * Instantiate a pushdata opcode from a - * buffer (this differs from fromData in - * that it will _always_ be a pushdata op). - * @param {Buffer} data - * @returns {Opcode} - */ - -Opcode.fromPush = function fromPush(data) { - if (data.length <= 0x4b) - return new Opcode(data.length, data); - - if (data.length <= 0xff) - return new Opcode(opcodes.OP_PUSHDATA1, data); - - if (data.length <= 0xffff) - return new Opcode(opcodes.OP_PUSHDATA2, data); - - if (data.length <= 0xffffffff) - return new Opcode(opcodes.OP_PUSHDATA4, data); - - throw new Error('Pushdata size too large.'); -}; - -/** - * Instantiate an opcode from a Number. - * @param {Number|BN} num - * @returns {Opcode} - */ - -Opcode.fromNumber = function fromNumber(num) { - return Opcode.fromData(Script.array(num)); -}; - -/** - * Instantiate an opcode from a small number. - * @param {Number} num - * @returns {Opcode} - */ - -Opcode.fromSmall = function fromSmall(num) { - assert(utils.isNumber(num) && num >= 0 && num <= 16); - return new Opcode(num === 0 ? 0 : num + 0x50); -}; - -/** - * Instantiate a pushdata opcode from a string. - * @param {String} data - * @returns {Opcode} - */ - -Opcode.fromString = function fromString(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - - return Opcode.fromData(data); -}; - -/** - * Instantiate a pushdata opcode from anything. - * @param {String|Buffer|Number|BN|Opcode} data - * @returns {Opcode} - */ - -Opcode.from = function from(data) { - if (data instanceof Opcode) - return data; - - if (typeof data === 'number') - return Opcode.fromOp(data); - - if (Buffer.isBuffer(data)) - return Opcode.fromData(data); - - if (typeof data === 'string') - return Opcode.fromString(data, 'utf8'); - - if (bn.isBN(data)) - return Opcode.fromNumber(data); - - assert(false, 'Bad data for opcode.'); -}; - -/** - * Test whether an object an Opcode. - * @param {Object} obj - * @returns {Boolean} - */ - -Opcode.isOpcode = function isOpcode(obj) { - return obj - && typeof obj.value === 'number' - && (Buffer.isBuffer(obj.data) || obj.data === null); -}; - -/** - * Witness Program - * @constructor - * @param {Number} version - * @param {Buffer} data - * @property {Number} version - Ranges from 0 to 16. - * @property {String|null} type - Null if malformed. `unknown` if unknown - * version (treated as anyone-can-spend). Otherwise one of `witnesspubkeyhash` - * or `witnessscripthash`. - * @property {Buffer} data - The hash (for now). - */ - -function Program(version, data) { - if (!(this instanceof Program)) - return new Program(version, data); - - assert(utils.isNumber(version)); - assert(Buffer.isBuffer(data)); - assert(version >= 0 && version <= 16); - assert(data.length >= 2 && data.length <= 40); - - this.version = version; - this.data = data; -} - -/** - * Get the witness program type. - * @returns {ScriptType} - */ - -Program.prototype.getType = function getType() { - if (this.version === 0) { - if (this.data.length === 20) - return scriptTypes.WITNESSPUBKEYHASH; - - if (this.data.length === 32) - return scriptTypes.WITNESSSCRIPTHASH; - - // Fail on bad version=0 - return scriptTypes.WITNESSMALFORMED; - } - - if (this.version === 1) { - if (this.data.length === 32) - return scriptTypes.WITNESSMASTHASH; - - // Fail on bad version=1 - return scriptTypes.WITNESSMALFORMED; - } - - // No interpretation of script (anyone can spend) - return scriptTypes.NONSTANDARD; -}; - -/** - * Test whether the program is either - * an unknown version or malformed. - * @returns {Boolean} - */ - -Program.prototype.isUnknown = function isUnknown() { - var type = this.getType(); - return type === scriptTypes.WITNESSMALFORMED - || type === scriptTypes.NONSTANDARD; -}; - -/** - * Test whether the program is malformed. - * @returns {Boolean} - */ - -Program.prototype.isMalformed = function isMalformed() { - return this.getType() === scriptTypes.WITNESSMALFORMED; -}; - -/** - * Inspect the program. - * @returns {String} - */ - -Program.prototype.inspect = function inspect() { - return ''; -}; - /* * Expose */ @@ -4964,9 +3619,4 @@ exports.types = scriptTypes; exports.typesByVal = constants.scriptTypesByVal; exports.flags = constants.flags; -exports.Script = Script; -exports.Opcode = Opcode; -exports.Stack = Stack; -exports.Witness = Witness; - module.exports = exports; diff --git a/lib/bcoin/sigcache.js b/lib/bcoin/script/sigcache.js similarity index 98% rename from lib/bcoin/sigcache.js rename to lib/bcoin/script/sigcache.js index 3d12729e..0df6e0e0 100644 --- a/lib/bcoin/sigcache.js +++ b/lib/bcoin/script/sigcache.js @@ -6,7 +6,7 @@ 'use strict'; -var bcoin = require('./env'); +var bcoin = require('../env'); var utils = bcoin.utils; var assert = utils.assert; diff --git a/lib/bcoin/script/stack.js b/lib/bcoin/script/stack.js new file mode 100644 index 00000000..f8aa648c --- /dev/null +++ b/lib/bcoin/script/stack.js @@ -0,0 +1,537 @@ +/*! + * script.js - script interpreter for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var constants = bcoin.constants; +var opcodes = constants.opcodes; +var ScriptError = bcoin.errors.ScriptError; + +/** + * Represents the stack of a Script during execution. + * @exports Stack + * @constructor + * @param {Buffer[]?} items - Stack items. + * @property {Buffer[]} items - Stack items. + * @property {Number} length - Size of stack. + */ + +function Stack(items) { + if (!(this instanceof Stack)) + return new Stack(items); + + this.items = items || []; +} + +Stack.prototype.__defineGetter__('length', function() { + return this.items.length; +}); + +Stack.prototype.__defineSetter__('length', function(length) { + return this.items.length = length; +}); + +/** + * Inspect the stack. + * @returns {String} Human-readable stack. + */ + +Stack.prototype.inspect = function inspect() { + return ''; +}; + +/** + * Convert the stack to a string. + * @returns {String} Human-readable stack. + */ + +Stack.prototype.toString = function toString() { + return bcoin.witness.format(this.items); +}; + +/** + * Format the stack as bitcoind asm. + * @param {Boolean?} decode - Attempt to decode hash types. + * @returns {String} Human-readable script. + */ + +Stack.prototype.toASM = function toASM(decode) { + return bcoin.script.formatASM(this.items, decode); +}; + +/** + * Pop the redeem script off the stack and deserialize it. + * @returns {Script|null} The redeem script. + */ + +Stack.prototype.getRedeem = function getRedeem() { + var redeem = this.items[this.items.length - 1]; + if (!redeem) + return; + return new bcoin.script(redeem); +}; + +/** + * Clone the stack. + * @returns {Stack} Cloned stack. + */ + +Stack.prototype.clone = function clone() { + return new Stack(this.items.slice()); +}; + +/** + * Get total size of the stack, including the alt stack. + * @param {Array} alt - Alt stack. + * @returns {Number} + */ + +Stack.prototype.getSize = function getSize(alt) { + return this.items.length + alt.length; +}; + +/** + * Push item onto stack. + * @see Array#push + * @param {Buffer} item + * @returns {Number} Stack size. + */ + +Stack.prototype.push = function push(item) { + return this.items.push(item); +}; + +/** + * Unshift item from stack. + * @see Array#unshift + * @param {Buffer} item + * @returns {Number} + */ + +Stack.prototype.unshift = function unshift(item) { + return this.items.unshift(item); +}; + +/** + * Slice out part of the stack items. + * @param {Number} start + * @param {Number} end + * @see Array#slice + * @returns {Buffer[]} + */ + +Stack.prototype.slice = function slice(start, end) { + return this.items.slice(start, end); +}; + +/** + * Splice stack items. + * @see Array#splice + * @param {Number} index + * @param {Number} remove + * @param {Buffer?} insert + * @returns {Buffer[]} + */ + +Stack.prototype.splice = function splice(i, remove, insert) { + if (insert === undefined) + return this.items.splice(i, remove); + return this.items.splice(i, remove, insert); +}; + +/** + * Pop a stack item. + * @see Array#pop + * @returns {Buffer|null} + */ + +Stack.prototype.pop = function pop() { + return this.items.pop(); +}; + +/** + * Shift a stack item. + * @see Array#shift + * @returns {Buffer|null} + */ + +Stack.prototype.shift = function shift() { + return this.items.shift(); +}; + +/** + * Get a stack item by index. + * @param {Number} index + * @returns {Buffer|null} + */ + +Stack.prototype.get = function get(i) { + return this.items[i]; +}; + +/** + * Get a stack item relative to + * the top of the stack. + * @example + * stack.top(-1); + * @param {Number} index + * @returns {Buffer|null} + */ + +Stack.prototype.top = function top(i) { + return this.items[this.items.length + i]; +}; + +/** + * Clear the stack. + */ + +Stack.prototype.clear = function clear() { + this.items.length = 0; +}; + +/** + * Set stack item at index. + * @param {Number} index + * @param {Buffer} value + * @returns {Buffer} + */ + +Stack.prototype.set = function set(i, value) { + return this.items[i] = value; +}; + +/** + * Swap stack values. + * @private + * @param {Number} i1 - Index 1. + * @param {Number} i2 - Index 2. + */ + +Stack.prototype._swap = function _swap(i1, i2) { + var v1, v2; + + i1 = this.items.length + i1; + i2 = this.items.length + i2; + + v1 = this.items[i1]; + v2 = this.items[i2]; + + this.items[i1] = v2; + this.items[i2] = v1; +}; + +/** + * Perform the OP_TOALTSTACK operation. + * @param {Array} alt - Alt stack. + * @throws {ScriptError} + */ + +Stack.prototype.toalt = function toalt(alt) { + if (this.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TOALTSTACK); + + alt.push(this.pop()); +}; + +/** + * Perform the OP_FROMALTSTACK operation. + * @param {Array} alt - Alt stack. + * @throws {ScriptError} + */ + +Stack.prototype.fromalt = function fromalt(alt) { + if (alt.length === 0) + throw new ScriptError('INVALID_ALTSTACK_OPERATION', opcodes.OP_FROMALTSTACK); + + this.push(alt.pop()); +}; + +/** + * Perform the OP_IFDUP operation. + * @throws {ScriptError} + */ + +Stack.prototype.ifdup = function ifdup() { + if (this.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_IFDUP); + + if (bcoin.script.bool(this.top(-1))) + this.push(this.top(-1)); +}; + +/** + * Perform the OP_DEPTH operation. + * @throws {ScriptError} + */ + +Stack.prototype.depth = function depth() { + this.push(bcoin.script.array(this.length)); +}; + +/** + * Perform the OP_DROP operation. + * @throws {ScriptError} + */ + +Stack.prototype.drop = function drop() { + if (this.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DROP); + + this.pop(); +}; + +/** + * Perform the OP_DUP operation. + * @throws {ScriptError} + */ + +Stack.prototype.dup = function dup() { + if (this.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DUP); + + this.push(this.top(-1)); +}; + +/** + * Perform the OP_NIP operation. + * @throws {ScriptError} + */ + +Stack.prototype.nip = function nip() { + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_NIP); + + this.splice(this.length - 2, 1); +}; + +/** + * Perform the OP_OVER operation. + * @throws {ScriptError} + */ + +Stack.prototype.over = function over() { + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_OVER); + + this.push(this.top(-2)); +}; + +/** + * Perform the OP_PICK operation. + * @param {VerifyFlags} flags + * @throws {ScriptError} + */ + +Stack.prototype.pick = function pick(flags) { + return this._pickroll(opcodes.OP_PICK, flags); +}; + +/** + * Perform the OP_ROLL operation. + * @param {VerifyFlags} flags + * @throws {ScriptError} + */ + +Stack.prototype.roll = function roll(flags) { + return this._pickroll(opcodes.OP_ROLL, flags); +}; + +/** + * Perform a pick or roll. + * @private + * @param {Number} op + * @param {VerifyFlags} flags + * @throws {ScriptError} + */ + +Stack.prototype._pickroll = function pickroll(op, flags) { + var val, n; + + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op); + + val = this.pop(); + n = bcoin.script.num(val, flags).toNumber(); + + if (n < 0 || n >= this.length) + throw new ScriptError('INVALID_STACK_OPERATION', op); + + val = this.top(-n - 1); + + if (op === opcodes.OP_ROLL) + this.splice(this.length - n - 1, 1); + + this.push(val); +}; + +/** + * Perform the OP_ROT operation. + * @throws {ScriptError} + */ + +Stack.prototype.rot = function rot() { + if (this.length < 3) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_ROT); + + this._swap(-3, -2); + this._swap(-2, -1); +}; + +/** + * Perform the OP_SWAP operation. + * @throws {ScriptError} + */ + +Stack.prototype.swap = function swap() { + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SWAP); + + this._swap(-2, -1); +}; + +/** + * Perform the OP_TUCK operation. + * @throws {ScriptError} + */ + +Stack.prototype.tuck = function tuck() { + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TUCK); + + this.splice(this.length - 2, 0, this.top(-1)); +}; + +/** + * Perform the OP_2DROP operation. + * @throws {ScriptError} + */ + +Stack.prototype.drop2 = function drop2() { + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DROP); + + this.pop(); + this.pop(); +}; + +/** + * Perform the OP_2DUP operation. + * @throws {ScriptError} + */ + +Stack.prototype.dup2 = function dup2() { + var v1, v2; + + if (this.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DUP); + + v1 = this.top(-2); + v2 = this.top(-1); + + this.push(v1); + this.push(v2); +}; + +/** + * Perform the OP_3DUP operation. + * @throws {ScriptError} + */ + +Stack.prototype.dup3 = function dup3() { + var v1, v2, v3; + + if (this.length < 3) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_3DUP); + + v1 = this.top(-3); + v2 = this.top(-2); + v3 = this.top(-1); + + this.push(v1); + this.push(v2); + this.push(v3); +}; + +/** + * Perform the OP_2OVER operation. + * @throws {ScriptError} + */ + +Stack.prototype.over2 = function over2() { + var v1, v2; + + if (this.length < 4) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2OVER); + + v1 = this.top(-4); + v2 = this.top(-3); + + this.push(v1); + this.push(v2); +}; + +/** + * Perform the OP_2ROT operation. + * @throws {ScriptError} + */ + +Stack.prototype.rot2 = function rot2() { + var v1, v2; + + if (this.length < 6) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2ROT); + + v1 = this.top(-6); + v2 = this.top(-5); + + this.splice(this.length - 6, 2); + this.push(v1); + this.push(v2); +}; + +/** + * Perform the OP_2SWAP operation. + * @throws {ScriptError} + */ + +Stack.prototype.swap2 = function swap2() { + if (this.length < 4) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2SWAP); + + this._swap(-4, -2); + this._swap(-3, -1); +}; + +/** + * Perform the OP_SIZE operation. + * @throws {ScriptError} + */ + +Stack.prototype.size = function size() { + if (this.length < 1) + throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SIZE); + + this.push(bcoin.script.array(this.top(-1).length)); +}; + +/** + * Test an object to see if it is a Stack. + * @param {Object} obj + * @returns {Boolean} + */ + +Stack.isStack = function isStack(obj) { + return obj && Array.isArray(obj.items) && typeof obj.swap2 === 'function'; +}; + +/* + * Expose + */ + +module.exports = Stack; diff --git a/lib/bcoin/script/witness.js b/lib/bcoin/script/witness.js new file mode 100644 index 00000000..69716e4c --- /dev/null +++ b/lib/bcoin/script/witness.js @@ -0,0 +1,613 @@ +/*! + * script.js - script interpreter for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var bn = require('bn.js'); +var constants = bcoin.constants; +var utils = require('../utils/utils'); +var assert = utils.assert; +var opcodes = constants.opcodes; +var STACK_FALSE = new Buffer([]); +var STACK_NEGATE = new Buffer([0x81]); +var scriptTypes = constants.scriptTypes; +var Script = require('./script'); +var Opcode = require('./opcode'); + +/** + * Refers to the witness field of segregated witness transactions. + * @exports Witness + * @constructor + * @param {Buffer[]|NakedWitness} items - Array of + * stack items. + * @property {Buffer[]} items + * @property {Script?} redeem + * @property {Number} length + */ + +function Witness(options) { + if (!(this instanceof Witness)) + return new Witness(options); + + this.items = []; + + if (options) + this.fromOptions(options); +} + +Witness.prototype.__defineGetter__('length', function() { + return this.items.length; +}); + +Witness.prototype.__defineSetter__('length', function(length) { + return this.items.length = length; +}); + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +Witness.prototype.fromOptions = function fromOptions(options) { + var items; + + assert(options, 'Witness data is required.'); + + items = options.items; + + if (!items) + items = options; + + if (items) + this.fromArray(items); + + return this; +}; + +/** + * Instantiate witness from options. + * @param {Object} options + * @returns {Witness} + */ + +Witness.fromOptions = function fromOptions(options) { + return new Witness().fromOptions(options); +}; + +/** + * Convert witness to an array of buffers. + * @returns {Buffer[]} + */ + +Witness.prototype.toArray = function toArray() { + return this.items.slice(); +}; + +/** + * Inject properties from an array of buffers. + * @private + * @param {Buffer[]} items + */ + +Witness.prototype.fromArray = function fromArray(items) { + assert(Array.isArray(items)); + this.items = items; + return this; +}; + +/** + * Insantiate witness from an array of buffers. + * @param {Buffer[]} items + * @returns {Witness} + */ + +Witness.fromArray = function fromArray(items) { + return new Witness().fromArray(items); +}; + +/** + * Inspect a Witness object. + * @returns {String} Human-readable script. + */ + +Witness.prototype.inspect = function inspect() { + return ''; +}; + +/** + * Convert a Witness object to a String. + * @returns {String} Human-readable script. + */ + +Witness.prototype.toString = function toString() { + return Witness.format(this.items); +}; + +/** + * Format the witness object as bitcoind asm. + * @param {Boolean?} decode - Attempt to decode hash types. + * @returns {String} Human-readable script. + */ + +Witness.prototype.toASM = function toASM(decode) { + return Script.formatASM(Script.parseArray(this.items), decode); +}; + +/** + * Clone the witness object. Note that the raw + * encoded witness data will be lost. This is + * because the function assumes you are going + * to be altering the stack items by hand. + * @returns {Witness} A clone of the current witness object. + */ + +Witness.prototype.clone = function clone() { + return new Witness(this.items.slice()); +}; + +/** + * Convert the Witness to a Stack object. + * This is usually done before executing + * a witness program. + * @returns {Stack} + */ + +Witness.prototype.toStack = function toStack() { + return new bcoin.stack(this.items.slice()); +}; + +/** + * "Guess" the type of the witness. + * This method is not 100% reliable. + * @returns {ScriptType} + */ + +Witness.prototype.getInputType = function getInputType() { + if (this.isPubkeyhashInput()) + return scriptTypes.WITNESSPUBKEYHASH; + + if (this.isScripthashInput()) + return scriptTypes.WITNESSSCRIPTHASH; + + return scriptTypes.NONSTANDARD; +}; + +/** + * "Guess" the address of the witness. + * This method is not 100% reliable. + * @returns {Address|null} + */ + +Witness.prototype.getInputAddress = function getInputAddress() { + return bcoin.address.fromWitness(this); +}; + +/** + * "Test" whether the witness is a pubkey input. + * Always returns false. + * @returns {Boolean} + */ + +Witness.prototype.isPubkeyInput = function isPubkeyInput() { + return false; +}; + +/** + * "Guess" whether the witness is a pubkeyhash input. + * This method is not 100% reliable. + * @returns {Boolean} + */ + +Witness.prototype.isPubkeyhashInput = function isPubkeyhashInput() { + return this.items.length === 2 + && Script.isSignatureEncoding(this.items[0]) + && Script.isKeyEncoding(this.items[1]); +}; + +/** + * "Test" whether the witness is a multisig input. + * Always returns false. + * @returns {Boolean} + */ + +Witness.prototype.isMultisigInput = function isMultisigInput() { + return false; +}; + +/** + * "Guess" whether the witness is a scripthash input. + * This method is not 100% reliable. + * @returns {Boolean} + */ + +Witness.prototype.isScripthashInput = function isScripthashInput() { + return this.items.length > 0 && !this.isPubkeyhashInput(); +}; + +/** + * "Guess" whether the witness is an unknown/non-standard type. + * This method is not 100% reliable. + * @returns {Boolean} + */ + +Witness.prototype.isUnknownInput = function isUnknownInput() { + return this.getInputType() === scriptTypes.NONSTANDARD; +}; + +/** + * Test the witness against a bloom filter. + * @param {Bloom} filter + * @returns {Boolean} + */ + +Witness.prototype.test = function test(filter) { + var i, chunk; + + for (i = 0; i < this.items.length; i++) { + chunk = this.items[i]; + + if (chunk.length === 0) + continue; + + if (filter.test(chunk)) + return true; + } + + return false; +}; + +/** + * Grab and deserialize the redeem script from the witness. + * @returns {Script} Redeem script. + */ + +Witness.prototype.getRedeem = function getRedeem() { + var redeem = this.items[this.items.length - 1]; + + if (!redeem) + return; + + return new Script(redeem); +}; + +/** + * Does nothing currently. + */ + +Witness.prototype.compile = function compile() { + // NOP +}; + +/** + * Find a data element in a witness. + * @param {Buffer} data - Data element to match against. + * @returns {Number} Index (`-1` if not present). + */ + +Witness.prototype.indexOf = function indexOf(data) { + return utils.indexOf(this.items, data); +}; + +/** + * Encode the witness to a Buffer. + * @param {String} enc - Encoding, either `'hex'` or `null`. + * @returns {Buffer|String} Serialized script. + */ + +Witness.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + var i; + + p.writeVarint(this.items.length); + + for (i = 0; i < this.items.length; i++) + p.writeVarBytes(this.items[i]); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Convert witness to a hex string. + * @returns {String} + */ + +Witness.prototype.toJSON = function toJSON() { + return this.toRaw().toString('hex'); +}; + +/** + * Inject properties from json object. + * @private + * @param {String} json + */ + +Witness.prototype.fromJSON = function fromJSON(json) { + assert(typeof json === 'string'); + return this.fromRaw(new Buffer(json, 'hex')); +}; + +/** + * Insantiate witness from a hex string. + * @param {String} json + * @returns {Witness} + */ + +Witness.fromJSON = function fromJSON(json) { + return new Witness().fromJSON(json); +}; + +/** + * Unshift an item onto the witness vector. + * @param {Number|String|Buffer|BN} data + * @returns {Number} + */ + +Witness.prototype.unshift = function unshift(data) { + return this.items.unshift(Witness.encodeItem(data)); +}; + +/** + * Push an item onto the witness vector. + * @param {Number|String|Buffer|BN} data + * @returns {Number} + */ + +Witness.prototype.push = function push(data) { + return this.items.push(Witness.encodeItem(data)); +}; + +/** + * Shift an item off the witness vector. + * @returns {Buffer} + */ + +Witness.prototype.shift = function shift() { + return this.items.shift(); +}; + +/** + * Shift an item off the witness vector. + * @returns {Buffer} + */ + +Witness.prototype.pop = function push(data) { + return this.items.pop(); +}; + +/** + * Remove an item from the witness vector. + * @param {Number} index + * @returns {Buffer} + */ + +Witness.prototype.remove = function remove(i) { + return this.items.splice(i, 1)[0]; +}; + +/** + * Insert an item into the witness vector. + * @param {Number} index + * @param {Number|String|Buffer|BN} data + */ + +Witness.prototype.insert = function insert(i, data) { + assert(i <= this.items.length, 'Index out of bounds.'); + this.items.splice(i, 0, Witness.encodeItem(data))[0]; +}; + +/** + * Get an item from the witness vector. + * @param {Number} index + * @returns {Buffer} + */ + +Witness.prototype.get = function get(i) { + return this.items[i]; +}; + +/** + * Get a small int (0-16) from the witness vector. + * @param {Number} index + * @returns {Number} `-1` on non-existent. + */ + +Witness.prototype.getSmall = function getSmall(i) { + var item = this.items[i]; + if (!item || item.length > 1) + return -1; + if (item.length === 0) + return 0; + if (!(item[0] >= 1 && item[1] <= 16)) + return -1; + return item[0]; +}; + +/** + * Get a number from the witness vector. + * @param {Number} index + * @returns {BN} + */ + +Witness.prototype.getNumber = function getNumber(i) { + var item = this.items[i]; + if (!item || item.length > 5) + return; + return Script.num(item, constants.flags.VERIFY_NONE, 5); +}; + +/** + * Get a string from the witness vector. + * @param {Number} index + * @returns {String} + */ + +Witness.prototype.getString = function getString(i) { + var item = this.items[i]; + if (!item) + return; + return item.toString('utf8'); +}; + +/** + * Clear the witness items. + */ + +Witness.prototype.clear = function clear() { + this.items.length = 0; +}; + +/** + * Set an item in the witness vector. + * @param {Number} index + * @param {Number|String|Buffer|BN} data + */ + +Witness.prototype.set = function set(i, data) { + assert(i <= this.items.length, 'Index out of bounds.'); + this.items[i] = Witness.encodeItem(data); +}; + +/** + * Encode a witness item. + * @param {Number|String|Buffer|BN} data + * @returns {Buffer} + */ + +Witness.encodeItem = function encodeItem(data) { + if (data instanceof Opcode) + data = data.data || data.value; + + if (typeof data === 'number') { + if (data === opcodes.OP_1NEGATE) + return STACK_NEGATE; + + if (data === opcodes.OP_0) + return STACK_FALSE; + + if (data >= opcodes.OP_1 && data <= opcodes.OP_16) + return new Buffer([data - 0x50]); + + throw new Error('Non-push opcode in witness.'); + } + + if (bn.isBN(data)) + return Script.array(data); + + if (typeof data === 'string') + return new Buffer(data, 'utf8'); + + return data; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +Witness.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var chunkCount = p.readVarint(); + var i; + + for (i = 0; i < chunkCount; i++) + this.items.push(p.readVarBytes()); + + return this; +}; + +/** + * Create a Witness from a serialized buffer. + * @param {Buffer|String} data - Serialized witness. + * @param {String?} enc - Either `"hex"` or `null`. + * @returns {Witness} + */ + +Witness.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new Witness().fromRaw(data); +}; + +/** + * Inject items from string. + * @private + * @param {String|String[]} items + */ + +Witness.prototype.fromString = function fromString(items) { + var i; + + if (!Array.isArray(items)) { + assert(typeof items === 'string'); + + items = items.trim(); + + if (items.length === 0) + return this; + + items = items.split(/\s+/); + } + + for (i = 0; i < items.length; i++) + this.items.push(new Buffer(items[i], 'hex')); + + return this; +}; + +/** + * Parse a test script/array + * string into a witness object. _Must_ + * contain only stack items (no non-push + * opcodes). + * @param {String|String[]} items - Script string. + * @returns {Witness} + * @throws Parse error. + */ + +Witness.fromString = function fromString(items) { + return new Witness().fromString(items); +}; + +/** + * Format script code into a human readable-string. + * @param {Array} code + * @returns {String} Human-readable string. + */ + +Witness.format = function format(items) { + var out = []; + var i; + + for (i = 0; i < items.length; i++) + out.push(items[i].toString('hex')); + + return out.join(' '); +}; + +/** + * Test an object to see if it is a Witness. + * @param {Object} obj + * @returns {Boolean} + */ + +Witness.isWitness = function isWitness(obj) { + return obj + && Array.isArray(obj.items) + && typeof obj.toStack === 'function'; +}; + +/* + * Expose + */ + +module.exports = Witness; diff --git a/lib/bcoin/wallet/account.js b/lib/bcoin/wallet/account.js new file mode 100644 index 00000000..c8245197 --- /dev/null +++ b/lib/bcoin/wallet/account.js @@ -0,0 +1,851 @@ +/*! + * account.js - account object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var assert = utils.assert; +var BufferReader = require('../utils/reader'); +var BufferWriter = require('../utils/writer'); + +/** + * Represents a BIP44 Account belonging to a {@link Wallet}. + * Note that this object does not enforce locks. Any method + * that does a write is internal API only and will lead + * to race conditions if used elsewhere. + * @exports Account + * @constructor + * @param {Object} options + * @param {WalletDB} options.db + * @param {HDPublicKey} options.accountKey + * @param {Boolean?} options.witness - Whether to use witness programs. + * @param {Number} options.accountIndex - The BIP44 account index. + * @param {Number?} options.receiveDepth - The index of the _next_ receiving + * address. + * @param {Number?} options.changeDepth - The index of the _next_ change + * address. + * @param {String?} options.type - Type of wallet (pubkeyhash, multisig) + * (default=pubkeyhash). + * @param {Number?} options.m - `m` value for multisig. + * @param {Number?} options.n - `n` value for multisig. + * @param {String?} options.wid - Wallet ID + * @param {String?} options.name - Account name + */ + +function Account(db, options) { + if (!(this instanceof Account)) + return new Account(db, options); + + assert(db, 'Database is required.'); + + this.db = db; + this.network = db.network; + this.lookahead = Account.MAX_LOOKAHEAD; + + this.receiveAddress = null; + this.changeAddress = null; + + this.wid = 0; + this.id = null; + this.name = null; + this.witness = this.db.options.witness; + this.accountKey = null; + this.accountIndex = 0; + this.receiveDepth = 0; + this.changeDepth = 0; + this.type = Account.types.PUBKEYHASH; + this.m = 1; + this.n = 1; + this.keys = []; + this.initialized = false; + + if (options) + this.fromOptions(options); +} + +/** + * Account types. + * @enum {Number} + * @default + */ + +Account.types = { + PUBKEYHASH: 0, + MULTISIG: 1 +}; + +/** + * Account types by value. + * @const {RevMap} + */ + +Account.typesByVal = { + 0: 'pubkeyhash', + 1: 'multisig' +}; + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +Account.prototype.fromOptions = function fromOptions(options) { + var i; + + assert(options, 'Options are required.'); + assert(utils.isNumber(options.wid)); + assert(utils.isName(options.id), 'Bad Wallet ID.'); + assert(bcoin.hd.isHD(options.accountKey), 'Account key is required.'); + assert(utils.isNumber(options.accountIndex), 'Account index is required.'); + + this.wid = options.wid; + this.id = options.id; + + if (options.name != null) { + assert(utils.isName(options.name), 'Bad account name.'); + this.name = options.name; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + + this.accountKey = options.accountKey; + + if (options.accountIndex != null) { + assert(utils.isNumber(options.accountIndex)); + this.accountIndex = options.accountIndex; + } + + if (options.receiveDepth != null) { + assert(utils.isNumber(options.receiveDepth)); + this.receiveDepth = options.receiveDepth; + } + + if (options.changeDepth != null) { + assert(utils.isNumber(options.changeDepth)); + this.changeDepth = options.changeDepth; + } + + if (options.type != null) { + if (typeof options.type === 'string') { + this.type = Account.types[options.type.toUpperCase()]; + assert(this.type != null); + } else { + assert(typeof options.type === 'number'); + this.type = options.type; + assert(Account.typesByVal[this.type]); + } + } + + if (options.m != null) { + assert(utils.isNumber(options.m)); + this.m = options.m; + } + + if (options.n != null) { + assert(utils.isNumber(options.n)); + this.n = options.n; + } + + if (options.initialized != null) { + assert(typeof options.initialized === 'boolean'); + this.initialized = options.initialized; + } + + if (this.n > 1) + this.type = Account.types.MULTISIG; + + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + + if (!this.name) + this.name = this.accountIndex + ''; + + if (options.keys) { + assert(Array.isArray(options.keys)); + for (i = 0; i < options.keys.length; i++) + this.pushKey(options.keys[i]); + } + + return this; +}; + +/** + * Instantiate account from options. + * @param {WalletDB} db + * @param {Object} options + * @returns {Account} + */ + +Account.fromOptions = function fromOptions(db, options) { + return new Account(db).fromOptions(options); +}; + +/* + * Default address lookahead. + * @const {Number} + */ + +Account.MAX_LOOKAHEAD = 5; + +/** + * Attempt to intialize the account (generating + * the first addresses along with the lookahead + * addresses). Called automatically from the + * walletdb. + * @param {Function} callback + */ + +Account.prototype.init = function init(callback) { + // Waiting for more keys. + if (this.keys.length !== this.n - 1) { + assert(!this.initialized); + this.save(); + return callback(); + } + + assert(this.receiveDepth === 0); + assert(this.changeDepth === 0); + + this.initialized = true; + this.setDepth(1, 1, callback); +}; + +/** + * Open the account (done after retrieval). + * @param {Function} callback + */ + +Account.prototype.open = function open(callback) { + if (!this.initialized) + return callback(); + + this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); + this.changeAddress = this.deriveChange(this.changeDepth - 1); + + callback(); +}; + +/** + * Add a public account key to the account (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). + * @throws Error on non-hdkey/non-accountkey. + */ + +Account.prototype.pushKey = function pushKey(key) { + var index; + + if (bcoin.hd.isExtended(key)) + key = bcoin.hd.fromBase58(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.'); + + if (key.equal(this.accountKey)) + throw new Error('Cannot add own key.'); + + index = utils.binaryInsert(this.keys, key, cmp, true); + + if (index === -1) + return false; + + if (this.keys.length > this.n - 1) { + utils.binaryRemove(this.keys, key, cmp); + throw new Error('Cannot add more keys.'); + } + + return true; +}; + +/** + * Remove a public account key to the account (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). + * @throws Error on non-hdkey/non-accountkey. + */ + +Account.prototype.spliceKey = function spliceKey(key) { + if (bcoin.hd.isExtended(key)) + key = bcoin.hd.fromBase58(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.'); + + if (key.equal(this.accountKey)) + throw new Error('Cannot remove own key.'); + + if (this.keys.length === this.n - 1) + throw new Error('Cannot remove key.'); + + return utils.binaryRemove(this.keys, key, cmp); +}; + +/** + * Add a public account key to the account (multisig). + * Saves the key in the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ + +Account.prototype.addKey = function addKey(key, callback) { + var self = this; + var result = false; + + try { + result = this.pushKey(key); + } catch (e) { + return callback(e); + } + + this._checkKeys(function(err, exists) { + if (err) + return callback(err); + + if (exists) { + self.spliceKey(key); + return callback(new Error('Cannot add a key from another account.')); + } + + // Try to initialize again. + self.init(function(err) { + if (err) + return callback(err); + + callback(null, result); + }); + }); +}; + +/** + * Ensure accounts are not sharing keys. + * @private + * @param {Function} callback + */ + +Account.prototype._checkKeys = function _checkKeys(callback) { + var self = this; + var ring, hash; + + if (this.initialized || this.type !== Account.types.MULTISIG) + return callback(null, false); + + if (this.keys.length !== this.n - 1) + return callback(null, false); + + ring = this.deriveReceive(0); + hash = ring.getScriptHash('hex'); + + this.db.getAddressPaths(hash, function(err, paths) { + if (err) + return callback(err); + + if (!paths) + return callback(null, false); + + callback(null, paths[self.wid] != null); + }); +}; + +/** + * Remove a public account key from the account (multisig). + * Remove the key from the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ + +Account.prototype.removeKey = function removeKey(key, callback) { + var result = false; + + try { + result = this.spliceKey(key); + } catch (e) { + return callback(e); + } + + this.save(); + + callback(null, result); +}; + +/** + * Create a new receiving address (increments receiveDepth). + * @returns {KeyRing} + */ + +Account.prototype.createReceive = function createReceive(callback) { + return this.createAddress(false, callback); +}; + +/** + * Create a new change address (increments receiveDepth). + * @returns {KeyRing} + */ + +Account.prototype.createChange = function createChange(callback) { + return this.createAddress(true, callback); +}; + +/** + * Create a new address (increments depth). + * @param {Boolean} change + * @param {Function} callback - Returns [Error, {@link KeyRing}]. + */ + +Account.prototype.createAddress = function createAddress(change, callback) { + var self = this; + var ring, lookahead; + + if (typeof change === 'function') { + callback = change; + change = false; + } + + if (change) { + ring = this.deriveChange(this.changeDepth); + lookahead = this.deriveChange(this.changeDepth + this.lookahead); + this.changeDepth++; + this.changeAddress = ring; + } else { + ring = this.deriveReceive(this.receiveDepth); + lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); + this.receiveDepth++; + this.receiveAddress = ring; + } + + this.saveAddress([ring, lookahead], function(err) { + if (err) + return callback(err); + + self.save(); + + callback(null, ring); + }); +}; + +/** + * Derive a receiving address at `index`. Do not increment depth. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveReceive = function deriveReceive(index, master) { + return this.deriveAddress(false, index, master); +}; + +/** + * Derive a change address at `index`. Do not increment depth. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveChange = function deriveChange(index, master) { + return this.deriveAddress(true, index, master); +}; + +/** + * Derive an address from `path` object. + * @param {Path} path + * @param {MasterKey} master + * @returns {KeyRing} + */ + +Account.prototype.derivePath = function derivePath(path, master) { + var ring, script, raw; + + // Imported key. + if (path.index === -1) { + assert(path.imported); + assert(this.type === Account.types.PUBKEYHASH); + + raw = path.imported; + + if (path.encrypted) + raw = master.decipher(raw, path.hash); + + if (!raw) + return; + + ring = bcoin.keyring.fromRaw(raw, this.network); + ring.path = path; + + return ring; + } + + // Custom redeem script. + if (path.script) + script = new bcoin.script(path.script); + + ring = this.deriveAddress(path.change, path.index, master, script); + + return ring; +}; + +/** + * Derive an address at `index`. Do not increment depth. + * @param {Boolean} change - Whether the address on the change branch. + * @param {Number} index + * @returns {KeyRing} + */ + +Account.prototype.deriveAddress = function deriveAddress(change, index, master, script) { + var keys = []; + var i, key, shared, ring; + + change = +change; + + if (master && master.key) { + key = master.key.deriveAccount44(this.accountIndex); + key = key.derive(change).derive(index); + } else { + key = this.accountKey.derive(change).derive(index); + } + + ring = bcoin.keyring.fromPublic(key.publicKey, this.network); + ring.witness = this.witness; + + if (script) { + // Custom redeem script. + assert(this.type === Account.types.PUBKEYHASH); + ring.script = script; + } else { + switch (this.type) { + case Account.types.PUBKEYHASH: + break; + case Account.types.MULTISIG: + keys.push(key.publicKey); + + for (i = 0; i < this.keys.length; i++) { + shared = this.keys[i]; + shared = shared.derive(change).derive(index); + keys.push(shared.publicKey); + } + + ring.script = bcoin.script.fromMultisig(this.m, this.n, keys); + + break; + } + } + + if (key.privateKey) + ring.privateKey = key.privateKey; + + ring.path = bcoin.path.fromAccount(this, ring, change, index); + + return ring; +}; + +/** + * Save the account to the database. Necessary + * when address depth and keys change. + * @param {Function} callback + */ + +Account.prototype.save = function save() { + return this.db.saveAccount(this); +}; + +/** + * Save addresses to path map. + * @param {KeyRing[]} rings + * @param {Function} callback + */ + +Account.prototype.saveAddress = function saveAddress(rings, callback) { + return this.db.saveAddress(this.wid, rings, callback); +}; + +/** + * Set change and receiving depth (depth is the index of the _next_ address). + * Allocate all addresses up to depth. Note that this also allocates + * new lookahead addresses. + * @param {Number} depth + * @param {Function} callback - Returns [Error, {@link KeyRing}, {@link KeyRing}]. + */ + +Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callback) { + var self = this; + var rings = []; + var i, receive, change; + + if (receiveDepth > this.receiveDepth) { + for (i = this.receiveDepth; i < receiveDepth; i++) { + receive = this.deriveReceive(i); + rings.push(receive); + } + + for (i = receiveDepth; i < receiveDepth + this.lookahead; i++) + rings.push(this.deriveReceive(i)); + + this.receiveAddress = receive; + this.receiveDepth = receiveDepth; + } + + if (changeDepth > this.changeDepth) { + for (i = this.changeDepth; i < changeDepth; i++) { + change = this.deriveChange(i); + rings.push(change); + } + + for (i = changeDepth; i < changeDepth + this.lookahead; i++) + rings.push(this.deriveChange(i)); + + this.changeAddress = change; + this.changeDepth = changeDepth; + } + + if (rings.length === 0) + return callback(); + + this.saveAddress(rings, function(err) { + if (err) + return callback(err); + + self.save(); + + callback(null, receive, change); + }); +}; + +/** + * Convert the account to a more inspection-friendly object. + * @returns {Object} + */ + +Account.prototype.inspect = function inspect() { + return { + wid: this.wid, + name: this.name, + network: this.network, + initialized: this.initialized, + type: Account.typesByVal[this.type].toLowerCase(), + m: this.m, + n: this.n, + address: this.initialized + ? this.receiveAddress.getAddress() + : null, + programAddress: this.initialized + ? this.receiveAddress.getProgramAddress() + : null, + witness: this.witness, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + accountKey: this.accountKey.xpubkey, + keys: this.keys.map(function(key) { + return key.xpubkey; + }) + }; +}; + +/** + * Convert the account to an object suitable for + * serialization. + * @returns {Object} + */ + +Account.prototype.toJSON = function toJSON() { + return { + network: this.network.type, + wid: this.wid, + name: this.name, + initialized: this.initialized, + type: Account.typesByVal[this.type].toLowerCase(), + m: this.m, + n: this.n, + witness: this.witness, + accountIndex: this.accountIndex, + receiveDepth: this.receiveDepth, + changeDepth: this.changeDepth, + receiveAddress: this.receiveAddress + ? this.receiveAddress.getAddress('base58') + : null, + programAddress: this.receiveAddress + ? this.receiveAddress.getProgramAddress('base58') + : null, + changeAddress: this.changeAddress + ? this.changeAddress.getAddress('base58') + : null, + accountKey: this.accountKey.xpubkey, + keys: this.keys.map(function(key) { + return key.xpubkey; + }) + }; +}; + +/** + * Inject properties from json object. + * @private + * @param {Object} json + */ + +Account.prototype.fromJSON = function fromJSON(json) { + var i, key; + + assert.equal(json.network, this.network.type); + assert(utils.isNumber(json.wid)); + assert(utils.isName(json.id), 'Bad wallet ID.'); + assert(utils.isName(json.name), 'Bad account name.'); + assert(typeof json.initialized === 'boolean'); + assert(typeof json.type === 'string'); + assert(utils.isNumber(json.m)); + assert(utils.isNumber(json.n)); + assert(typeof json.witness === 'boolean'); + assert(utils.isNumber(json.accountIndex)); + assert(utils.isNumber(json.receiveDepth)); + assert(utils.isNumber(json.changeDepth)); + assert(Array.isArray(json.keys)); + + this.wid = json.wid; + this.name = json.name; + this.initialized = json.initialized; + this.type = Account.types[json.type.toUpperCase()]; + this.m = json.m; + this.n = json.n; + this.witness = json.witness; + this.accountIndex = json.accountIndex; + this.receiveDepth = json.receiveDepth; + this.changeDepth = json.changeDepth; + this.accountKey = bcoin.hd.fromBase58(json.accountKey); + + assert(this.type != null); + + for (i = 0; i < json.keys.length; i++) { + key = bcoin.hd.fromBase58(json.keys[i]); + this.pushKey(key); + } + + return this; +}; + +/** + * Serialize the account. + * @returns {Buffer} + */ + +Account.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + var i, key; + + p.writeU32(this.network.magic); + p.writeVarString(this.name, 'utf8'); + p.writeU8(this.initialized ? 1 : 0); + p.writeU8(this.type); + p.writeU8(this.m); + p.writeU8(this.n); + p.writeU8(this.witness ? 1 : 0); + p.writeU32(this.accountIndex); + p.writeU32(this.receiveDepth); + p.writeU32(this.changeDepth); + p.writeBytes(this.accountKey.toRaw()); + p.writeU8(this.keys.length); + + for (i = 0; i < this.keys.length; i++) { + key = this.keys[i]; + p.writeBytes(key.toRaw()); + } + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @returns {Object} + */ + +Account.prototype.fromRaw = function fromRaw(data) { + var p = new BufferReader(data); + var i, count, key; + + this.network = bcoin.network.fromMagic(p.readU32()); + this.name = p.readVarString('utf8'); + this.initialized = p.readU8() === 1; + this.type = p.readU8(); + this.m = p.readU8(); + this.n = p.readU8(); + this.witness = p.readU8() === 1; + this.accountIndex = p.readU32(); + this.receiveDepth = p.readU32(); + this.changeDepth = p.readU32(); + this.accountKey = bcoin.hd.fromRaw(p.readBytes(82)); + + assert(Account.typesByVal[this.type]); + + count = p.readU8(); + + for (i = 0; i < count; i++) { + key = bcoin.hd.fromRaw(p.readBytes(82)); + this.pushKey(key); + } + + return this; +}; + +/** + * Instantiate a account from serialized data. + * @param {WalletDB} data + * @param {Buffer} data + * @returns {Account} + */ + +Account.fromRaw = function fromRaw(db, data) { + return new Account(db).fromRaw(data); +}; + +/** + * Instantiate a Account from a + * jsonified account object. + * @param {WalletDB} db + * @param {Object} json - The jsonified account object. + * @returns {Account} + */ + +Account.fromJSON = function fromJSON(db, json) { + return new Account(db).fromJSON(json); +}; + +/** + * Test an object to see if it is a Account. + * @param {Object} obj + * @returns {Boolean} + */ + +Account.isAccount = function isAccount(obj) { + return obj + && typeof obj.receiveDepth === 'number' + && obj.deriveAddress === 'function'; +}; + +/* + * Helpers + */ + +function cmp(key1, key2) { + return key1.compare(key2); +} + +/* + * Expose + */ + +module.exports = Account; diff --git a/lib/bcoin/wallet/path.js b/lib/bcoin/wallet/path.js new file mode 100644 index 00000000..1228b027 --- /dev/null +++ b/lib/bcoin/wallet/path.js @@ -0,0 +1,285 @@ +/*! + * path.js - path object for wallets + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var utils = require('../utils/utils'); +var assert = utils.assert; +var constants = bcoin.constants; +var BufferReader = require('../utils/reader'); +var BufferWriter = require('../utils/writer'); + +/** + * Path + * @constructor + * @private + * @property {WalletID} wid + * @property {String} name - Account name. + * @property {Number} account - Account index. + * @property {Number} change - Change index. + * @property {Number} index - Address index. + * @property {Address|null} address + */ + +function Path() { + if (!(this instanceof Path)) + return new Path(); + + this.wid = null; + this.name = null; + this.account = 0; + this.change = -1; + this.index = -1; + + this.encrypted = false; + this.imported = null; + this.script = null; + + // Currently unused. + this.type = bcoin.script.types.PUBKEYHASH; + this.version = -1; + + // Passed in by caller. + this.id = null; + this.hash = null; +} + +/** + * Clone the path object. + * @returns {Path} + */ + +Path.prototype.clone = function clone() { + var path = new Path(); + + path.wid = this.wid; + path.name = this.name; + path.account = this.account; + path.change = this.change; + path.index = this.index; + + path.encrypted = this.encrypted; + path.imported = this.imported; + path.script = this.script; + + path.type = this.type; + path.version = this.version; + + path.id = this.id; + path.hash = this.hash; + + return path; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +Path.prototype.fromRaw = function fromRaw(data) { + var p = new BufferReader(data); + + this.wid = p.readU32(); + this.name = p.readVarString('utf8'); + this.account = p.readU32(); + + switch (p.readU8()) { + case 0: + this.change = p.readU32(); + this.index = p.readU32(); + if (p.readU8() === 1) + this.script = p.readVarBytes(); + break; + case 1: + this.encrypted = p.readU8() === 1; + this.imported = p.readVarBytes(); + this.change = -1; + this.index = -1; + break; + default: + assert(false); + break; + } + + this.version = p.read8(); + this.type = p.readU8(); + + return this; +}; + +/** + * Instantiate path from serialized data. + * @param {Buffer} data + * @returns {Path} + */ + +Path.fromRaw = function fromRaw(data) { + return new Path().fromRaw(data); +}; + +/** + * Serialize path. + * @returns {Buffer} + */ + +Path.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + + p.writeU32(this.wid); + p.writeVarString(this.name, 'utf8'); + p.writeU32(this.account); + + if (this.index !== -1) { + assert(!this.imported); + p.writeU8(0); + p.writeU32(this.change); + p.writeU32(this.index); + if (this.script) { + p.writeU8(1); + p.writeVarBytes(this.script); + } else { + p.writeU8(0); + } + } else { + assert(this.imported); + p.writeU8(1); + p.writeU8(this.encrypted ? 1 : 0); + p.writeVarBytes(this.imported); + } + + p.write8(this.version); + p.writeU8(this.type); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from account. + * @private + * @param {WalletID} wid + * @param {KeyRing} ring + */ + +Path.prototype.fromAccount = function fromAccount(account, ring, change, index) { + this.wid = account.wid; + this.name = account.name; + this.account = account.accountIndex; + + if (change != null) + this.change = change; + + if (index != null) + this.index = index; + + 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 {KeyRing} ring + * @returns {Path} + */ + +Path.fromAccount = function fromAccount(account, ring, change, index) { + return new Path().fromAccount(account, ring, change, index); +}; + +/** + * Convert path object to string derivation path. + * @returns {String} + */ + +Path.prototype.toPath = function toPath() { + return 'm/' + this.account + + '\'/' + this.change + + '/' + this.index; +}; + +/** + * Convert path object to an address (currently unused). + * @returns {Address} + */ + +Path.prototype.toAddress = function toAddress(network) { + return bcoin.address.fromHash(this.hash, this.type, this.version, network); +}; + +/** + * Convert path to a json-friendly object. + * @returns {Object} + */ + +Path.prototype.toJSON = function toJSON() { + return { + name: this.name, + change: this.change === 1, + path: this.toPath() + }; +}; + +/** + * 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} + */ + +Path.prototype.inspect = function() { + return ''; +}; + +/** + * Expose + */ + +module.exports = Path; diff --git a/lib/bcoin/wallet/txdb.js b/lib/bcoin/wallet/txdb.js index 24c6d8d5..f5f71d43 100644 --- a/lib/bcoin/wallet/txdb.js +++ b/lib/bcoin/wallet/txdb.js @@ -2327,120 +2327,6 @@ TXDB.prototype.abandon = function abandon(hash, callback) { }); }; -/** - * Details - */ - -function Details(info) { - if (!(this instanceof Details)) - return new Details(info); - - this.db = info.db; - this.network = info.db.network; - this.wid = info.wid; - this.id = info.id; - this.hash = info.tx.hash('hex'); - this.height = info.tx.height; - this.block = info.tx.block; - this.index = info.tx.index; - this.confirmations = info.tx.getConfirmations(this.db.height); - this.fee = info.tx.getFee(); - this.ts = info.tx.ts; - this.ps = info.tx.ps; - this.tx = info.tx; - this.inputs = []; - this.outputs = []; - - this.init(info.table); -} - -Details.prototype.init = function init(table) { - this._insert(this.tx.inputs, this.inputs, table); - this._insert(this.tx.outputs, this.outputs, table); -}; - -Details.prototype._insert = function _insert(vector, target, table) { - var i, j, io, address, hash, paths, path, member; - - for (i = 0; i < vector.length; i++) { - io = vector[i]; - member = new DetailsMember(); - - if (io instanceof bcoin.input) - member.value = io.coin ? io.coin.value : 0; - else - member.value = io.value; - - address = io.getAddress(); - - if (address) { - member.address = address; - - hash = address.getHash('hex'); - paths = table[hash]; - - for (j = 0; j < paths.length; j++) { - path = paths[j]; - if (path.wid === this.wid) { - path.id = this.id; - member.path = path; - break; - } - } - } - - target.push(member); - } -}; - -Details.prototype.toJSON = function toJSON() { - var self = this; - return { - wid: this.wid, - id: this.id, - hash: utils.revHex(this.hash), - height: this.height, - block: this.block ? utils.revHex(this.block) : null, - ts: this.ts, - ps: this.ps, - index: this.index, - fee: utils.btc(this.fee), - confirmations: this.confirmations, - inputs: this.inputs.map(function(input) { - return input.toJSON(self.network); - }), - outputs: this.outputs.map(function(output) { - return output.toJSON(self.network); - }), - tx: this.tx.toRaw().toString('hex') - }; -}; - -/** - * DetailsMember - */ - -function DetailsMember() { - if (!(this instanceof DetailsMember)) - return new DetailsMember(); - - this.value = 0; - this.address = null; - this.path = null; -} - -DetailsMember.prototype.toJSON = function toJSON(network) { - return { - value: utils.btc(this.value), - address: this.address - ? this.address.toBase58(network) - : null, - path: this.path - ? this.path.toJSON() - : null - }; -}; - /* * Balance */ @@ -2522,5 +2408,4 @@ function sortCoins(coins) { * Expose */ -TXDB.Details = Details; module.exports = TXDB; diff --git a/lib/bcoin/wallet/wallet.js b/lib/bcoin/wallet/wallet.js index a5d48b52..ff99c86d 100644 --- a/lib/bcoin/wallet/wallet.js +++ b/lib/bcoin/wallet/wallet.js @@ -15,6 +15,7 @@ var assert = utils.assert; var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); var TXDB = require('./txdb'); +var Path = require('./path'); /** * BIP44 Wallet @@ -834,7 +835,7 @@ Wallet.prototype.importKey = function importKey(account, ring, passphrase, callb if (!account) return callback(new Error('Account not found.')); - if (account.type !== Account.types.PUBKEYHASH) + if (account.type !== bcoin.account.types.PUBKEYHASH) return callback(new Error('Cannot import into non-pkh account.')); self.unlock(passphrase, null, function(err) { @@ -842,7 +843,7 @@ Wallet.prototype.importKey = function importKey(account, ring, passphrase, callb return callback(err); raw = ring.toRaw(); - path = bcoin.path.fromAccount(account, ring); + path = Path.fromAccount(account, ring); if (self.master.encrypted) { raw = self.master.encipher(raw, path.hash); @@ -2112,830 +2113,6 @@ Wallet.isWallet = function isWallet(obj) { && obj.template === 'function'; }; -/** - * Represents a BIP44 Account belonging to a {@link Wallet}. - * Note that this object does not enforce locks. Any method - * that does a write is internal API only and will lead - * to race conditions if used elsewhere. - * @exports Account - * @constructor - * @param {Object} options - * @param {WalletDB} options.db - * @param {HDPublicKey} options.accountKey - * @param {Boolean?} options.witness - Whether to use witness programs. - * @param {Number} options.accountIndex - The BIP44 account index. - * @param {Number?} options.receiveDepth - The index of the _next_ receiving - * address. - * @param {Number?} options.changeDepth - The index of the _next_ change - * address. - * @param {String?} options.type - Type of wallet (pubkeyhash, multisig) - * (default=pubkeyhash). - * @param {Number?} options.m - `m` value for multisig. - * @param {Number?} options.n - `n` value for multisig. - * @param {String?} options.wid - Wallet ID - * @param {String?} options.name - Account name - */ - -function Account(db, options) { - if (!(this instanceof Account)) - return new Account(db, options); - - assert(db, 'Database is required.'); - - this.db = db; - this.network = db.network; - this.lookahead = Account.MAX_LOOKAHEAD; - - this.receiveAddress = null; - this.changeAddress = null; - - this.wid = 0; - this.id = null; - this.name = null; - this.witness = this.db.options.witness; - this.accountKey = null; - this.accountIndex = 0; - this.receiveDepth = 0; - this.changeDepth = 0; - this.type = Account.types.PUBKEYHASH; - this.m = 1; - this.n = 1; - this.keys = []; - this.initialized = false; - - if (options) - this.fromOptions(options); -} - -/** - * Account types. - * @enum {Number} - * @default - */ - -Account.types = { - PUBKEYHASH: 0, - MULTISIG: 1 -}; - -/** - * Account types by value. - * @const {RevMap} - */ - -Account.typesByVal = { - 0: 'pubkeyhash', - 1: 'multisig' -}; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -Account.prototype.fromOptions = function fromOptions(options) { - var i; - - assert(options, 'Options are required.'); - assert(utils.isNumber(options.wid)); - assert(utils.isName(options.id), 'Bad Wallet ID.'); - assert(bcoin.hd.isHD(options.accountKey), 'Account key is required.'); - assert(utils.isNumber(options.accountIndex), 'Account index is required.'); - - this.wid = options.wid; - this.id = options.id; - - if (options.name != null) { - assert(utils.isName(options.name), 'Bad account name.'); - this.name = options.name; - } - - if (options.witness != null) { - assert(typeof options.witness === 'boolean'); - this.witness = options.witness; - } - - this.accountKey = options.accountKey; - - if (options.accountIndex != null) { - assert(utils.isNumber(options.accountIndex)); - this.accountIndex = options.accountIndex; - } - - if (options.receiveDepth != null) { - assert(utils.isNumber(options.receiveDepth)); - this.receiveDepth = options.receiveDepth; - } - - if (options.changeDepth != null) { - assert(utils.isNumber(options.changeDepth)); - this.changeDepth = options.changeDepth; - } - - if (options.type != null) { - if (typeof options.type === 'string') { - this.type = Account.types[options.type.toUpperCase()]; - assert(this.type != null); - } else { - assert(typeof options.type === 'number'); - this.type = options.type; - assert(Account.typesByVal[this.type]); - } - } - - if (options.m != null) { - assert(utils.isNumber(options.m)); - this.m = options.m; - } - - if (options.n != null) { - assert(utils.isNumber(options.n)); - this.n = options.n; - } - - if (options.initialized != null) { - assert(typeof options.initialized === 'boolean'); - this.initialized = options.initialized; - } - - if (this.n > 1) - this.type = Account.types.MULTISIG; - - if (this.m < 1 || this.m > this.n) - throw new Error('m ranges between 1 and n'); - - if (!this.name) - this.name = this.accountIndex + ''; - - if (options.keys) { - assert(Array.isArray(options.keys)); - for (i = 0; i < options.keys.length; i++) - this.pushKey(options.keys[i]); - } - - return this; -}; - -/** - * Instantiate account from options. - * @param {WalletDB} db - * @param {Object} options - * @returns {Account} - */ - -Account.fromOptions = function fromOptions(db, options) { - return new Account(db).fromOptions(options); -}; - -/* - * Default address lookahead. - * @const {Number} - */ - -Account.MAX_LOOKAHEAD = 5; - -/** - * Attempt to intialize the account (generating - * the first addresses along with the lookahead - * addresses). Called automatically from the - * walletdb. - * @param {Function} callback - */ - -Account.prototype.init = function init(callback) { - // Waiting for more keys. - if (this.keys.length !== this.n - 1) { - assert(!this.initialized); - this.save(); - return callback(); - } - - assert(this.receiveDepth === 0); - assert(this.changeDepth === 0); - - this.initialized = true; - this.setDepth(1, 1, callback); -}; - -/** - * Open the account (done after retrieval). - * @param {Function} callback - */ - -Account.prototype.open = function open(callback) { - if (!this.initialized) - return callback(); - - this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); - this.changeAddress = this.deriveChange(this.changeDepth - 1); - - callback(); -}; - -/** - * Add a public account key to the account (multisig). - * Does not update the database. - * @param {HDPublicKey} key - Account (bip44) - * key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ - -Account.prototype.pushKey = function pushKey(key) { - var index; - - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(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.'); - - if (key.equal(this.accountKey)) - throw new Error('Cannot add own key.'); - - index = utils.binaryInsert(this.keys, key, cmp, true); - - if (index === -1) - return false; - - if (this.keys.length > this.n - 1) { - utils.binaryRemove(this.keys, key, cmp); - throw new Error('Cannot add more keys.'); - } - - return true; -}; - -/** - * Remove a public account key to the account (multisig). - * Does not update the database. - * @param {HDPublicKey} key - Account (bip44) - * key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ - -Account.prototype.spliceKey = function spliceKey(key) { - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(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.'); - - if (key.equal(this.accountKey)) - throw new Error('Cannot remove own key.'); - - if (this.keys.length === this.n - 1) - throw new Error('Cannot remove key.'); - - return utils.binaryRemove(this.keys, key, cmp); -}; - -/** - * Add a public account key to the account (multisig). - * Saves the key in the wallet database. - * @param {HDPublicKey} key - * @param {Function} callback - */ - -Account.prototype.addKey = function addKey(key, callback) { - var self = this; - var result = false; - - try { - result = this.pushKey(key); - } catch (e) { - return callback(e); - } - - this._checkKeys(function(err, exists) { - if (err) - return callback(err); - - if (exists) { - self.spliceKey(key); - return callback(new Error('Cannot add a key from another account.')); - } - - // Try to initialize again. - self.init(function(err) { - if (err) - return callback(err); - - callback(null, result); - }); - }); -}; - -/** - * Ensure accounts are not sharing keys. - * @private - * @param {Function} callback - */ - -Account.prototype._checkKeys = function _checkKeys(callback) { - var self = this; - var ring, hash; - - if (this.initialized || this.type !== Account.types.MULTISIG) - return callback(null, false); - - if (this.keys.length !== this.n - 1) - return callback(null, false); - - ring = this.deriveReceive(0); - hash = ring.getScriptHash('hex'); - - this.db.getAddressPaths(hash, function(err, paths) { - if (err) - return callback(err); - - if (!paths) - return callback(null, false); - - callback(null, paths[self.wid] != null); - }); -}; - -/** - * Remove a public account key from the account (multisig). - * Remove the key from the wallet database. - * @param {HDPublicKey} key - * @param {Function} callback - */ - -Account.prototype.removeKey = function removeKey(key, callback) { - var result = false; - - try { - result = this.spliceKey(key); - } catch (e) { - return callback(e); - } - - this.save(); - - callback(null, result); -}; - -/** - * Create a new receiving address (increments receiveDepth). - * @returns {KeyRing} - */ - -Account.prototype.createReceive = function createReceive(callback) { - return this.createAddress(false, callback); -}; - -/** - * Create a new change address (increments receiveDepth). - * @returns {KeyRing} - */ - -Account.prototype.createChange = function createChange(callback) { - return this.createAddress(true, callback); -}; - -/** - * Create a new address (increments depth). - * @param {Boolean} change - * @param {Function} callback - Returns [Error, {@link KeyRing}]. - */ - -Account.prototype.createAddress = function createAddress(change, callback) { - var self = this; - var ring, lookahead; - - if (typeof change === 'function') { - callback = change; - change = false; - } - - if (change) { - ring = this.deriveChange(this.changeDepth); - lookahead = this.deriveChange(this.changeDepth + this.lookahead); - this.changeDepth++; - this.changeAddress = ring; - } else { - ring = this.deriveReceive(this.receiveDepth); - lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); - this.receiveDepth++; - this.receiveAddress = ring; - } - - this.saveAddress([ring, lookahead], function(err) { - if (err) - return callback(err); - - self.save(); - - callback(null, ring); - }); -}; - -/** - * Derive a receiving address at `index`. Do not increment depth. - * @param {Number} index - * @returns {KeyRing} - */ - -Account.prototype.deriveReceive = function deriveReceive(index, master) { - return this.deriveAddress(false, index, master); -}; - -/** - * Derive a change address at `index`. Do not increment depth. - * @param {Number} index - * @returns {KeyRing} - */ - -Account.prototype.deriveChange = function deriveChange(index, master) { - return this.deriveAddress(true, index, master); -}; - -/** - * Derive an address from `path` object. - * @param {Path} path - * @param {MasterKey} master - * @returns {KeyRing} - */ - -Account.prototype.derivePath = function derivePath(path, master) { - var ring, script, raw; - - // Imported key. - if (path.index === -1) { - assert(path.imported); - assert(this.type === Account.types.PUBKEYHASH); - - raw = path.imported; - - if (path.encrypted) - raw = master.decipher(raw, path.hash); - - if (!raw) - return; - - ring = bcoin.keyring.fromRaw(raw, this.network); - ring.path = path; - - return ring; - } - - // Custom redeem script. - if (path.script) - script = new bcoin.script(path.script); - - ring = this.deriveAddress(path.change, path.index, master, script); - - return ring; -}; - -/** - * Derive an address at `index`. Do not increment depth. - * @param {Boolean} change - Whether the address on the change branch. - * @param {Number} index - * @returns {KeyRing} - */ - -Account.prototype.deriveAddress = function deriveAddress(change, index, master, script) { - var keys = []; - var i, key, shared, ring; - - change = +change; - - if (master && master.key) { - key = master.key.deriveAccount44(this.accountIndex); - key = key.derive(change).derive(index); - } else { - key = this.accountKey.derive(change).derive(index); - } - - ring = bcoin.keyring.fromPublic(key.publicKey, this.network); - ring.witness = this.witness; - - if (script) { - // Custom redeem script. - assert(this.type === Account.types.PUBKEYHASH); - ring.script = script; - } else { - switch (this.type) { - case Account.types.PUBKEYHASH: - break; - case Account.types.MULTISIG: - keys.push(key.publicKey); - - for (i = 0; i < this.keys.length; i++) { - shared = this.keys[i]; - shared = shared.derive(change).derive(index); - keys.push(shared.publicKey); - } - - ring.script = bcoin.script.fromMultisig(this.m, this.n, keys); - - break; - } - } - - if (key.privateKey) - ring.privateKey = key.privateKey; - - ring.path = bcoin.path.fromAccount(this, ring, change, index); - - return ring; -}; - -/** - * Save the account to the database. Necessary - * when address depth and keys change. - * @param {Function} callback - */ - -Account.prototype.save = function save() { - return this.db.saveAccount(this); -}; - -/** - * Save addresses to path map. - * @param {KeyRing[]} rings - * @param {Function} callback - */ - -Account.prototype.saveAddress = function saveAddress(rings, callback) { - return this.db.saveAddress(this.wid, rings, callback); -}; - -/** - * Set change and receiving depth (depth is the index of the _next_ address). - * Allocate all addresses up to depth. Note that this also allocates - * new lookahead addresses. - * @param {Number} depth - * @param {Function} callback - Returns [Error, {@link KeyRing}, {@link KeyRing}]. - */ - -Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callback) { - var self = this; - var rings = []; - var i, receive, change; - - if (receiveDepth > this.receiveDepth) { - for (i = this.receiveDepth; i < receiveDepth; i++) { - receive = this.deriveReceive(i); - rings.push(receive); - } - - for (i = receiveDepth; i < receiveDepth + this.lookahead; i++) - rings.push(this.deriveReceive(i)); - - this.receiveAddress = receive; - this.receiveDepth = receiveDepth; - } - - if (changeDepth > this.changeDepth) { - for (i = this.changeDepth; i < changeDepth; i++) { - change = this.deriveChange(i); - rings.push(change); - } - - for (i = changeDepth; i < changeDepth + this.lookahead; i++) - rings.push(this.deriveChange(i)); - - this.changeAddress = change; - this.changeDepth = changeDepth; - } - - if (rings.length === 0) - return callback(); - - this.saveAddress(rings, function(err) { - if (err) - return callback(err); - - self.save(); - - callback(null, receive, change); - }); -}; - -/** - * Convert the account to a more inspection-friendly object. - * @returns {Object} - */ - -Account.prototype.inspect = function inspect() { - return { - wid: this.wid, - name: this.name, - network: this.network, - initialized: this.initialized, - type: Account.typesByVal[this.type].toLowerCase(), - m: this.m, - n: this.n, - address: this.initialized - ? this.receiveAddress.getAddress() - : null, - programAddress: this.initialized - ? this.receiveAddress.getProgramAddress() - : null, - witness: this.witness, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, - accountKey: this.accountKey.xpubkey, - keys: this.keys.map(function(key) { - return key.xpubkey; - }) - }; -}; - -/** - * Convert the account to an object suitable for - * serialization. - * @returns {Object} - */ - -Account.prototype.toJSON = function toJSON() { - return { - network: this.network.type, - wid: this.wid, - name: this.name, - initialized: this.initialized, - type: Account.typesByVal[this.type].toLowerCase(), - m: this.m, - n: this.n, - witness: this.witness, - accountIndex: this.accountIndex, - receiveDepth: this.receiveDepth, - changeDepth: this.changeDepth, - receiveAddress: this.receiveAddress - ? this.receiveAddress.getAddress('base58') - : null, - programAddress: this.receiveAddress - ? this.receiveAddress.getProgramAddress('base58') - : null, - changeAddress: this.changeAddress - ? this.changeAddress.getAddress('base58') - : null, - accountKey: this.accountKey.xpubkey, - keys: this.keys.map(function(key) { - return key.xpubkey; - }) - }; -}; - -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -Account.prototype.fromJSON = function fromJSON(json) { - var i, key; - - assert.equal(json.network, this.network.type); - assert(utils.isNumber(json.wid)); - assert(utils.isName(json.id), 'Bad wallet ID.'); - assert(utils.isName(json.name), 'Bad account name.'); - assert(typeof json.initialized === 'boolean'); - assert(typeof json.type === 'string'); - assert(utils.isNumber(json.m)); - assert(utils.isNumber(json.n)); - assert(typeof json.witness === 'boolean'); - assert(utils.isNumber(json.accountIndex)); - assert(utils.isNumber(json.receiveDepth)); - assert(utils.isNumber(json.changeDepth)); - assert(Array.isArray(json.keys)); - - this.wid = json.wid; - this.name = json.name; - this.initialized = json.initialized; - this.type = Account.types[json.type.toUpperCase()]; - this.m = json.m; - this.n = json.n; - this.witness = json.witness; - this.accountIndex = json.accountIndex; - this.receiveDepth = json.receiveDepth; - this.changeDepth = json.changeDepth; - this.accountKey = bcoin.hd.fromBase58(json.accountKey); - - assert(this.type != null); - - for (i = 0; i < json.keys.length; i++) { - key = bcoin.hd.fromBase58(json.keys[i]); - this.pushKey(key); - } - - return this; -}; - -/** - * Serialize the account. - * @returns {Buffer} - */ - -Account.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - var i, key; - - p.writeU32(this.network.magic); - p.writeVarString(this.name, 'utf8'); - p.writeU8(this.initialized ? 1 : 0); - p.writeU8(this.type); - p.writeU8(this.m); - p.writeU8(this.n); - p.writeU8(this.witness ? 1 : 0); - p.writeU32(this.accountIndex); - p.writeU32(this.receiveDepth); - p.writeU32(this.changeDepth); - p.writeBytes(this.accountKey.toRaw()); - p.writeU8(this.keys.length); - - for (i = 0; i < this.keys.length; i++) { - key = this.keys[i]; - p.writeBytes(key.toRaw()); - } - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @returns {Object} - */ - -Account.prototype.fromRaw = function fromRaw(data) { - var p = new BufferReader(data); - var i, count, key; - - this.network = bcoin.network.fromMagic(p.readU32()); - this.name = p.readVarString('utf8'); - this.initialized = p.readU8() === 1; - this.type = p.readU8(); - this.m = p.readU8(); - this.n = p.readU8(); - this.witness = p.readU8() === 1; - this.accountIndex = p.readU32(); - this.receiveDepth = p.readU32(); - this.changeDepth = p.readU32(); - this.accountKey = bcoin.hd.fromRaw(p.readBytes(82)); - - assert(Account.typesByVal[this.type]); - - count = p.readU8(); - - for (i = 0; i < count; i++) { - key = bcoin.hd.fromRaw(p.readBytes(82)); - this.pushKey(key); - } - - return this; -}; - -/** - * Instantiate a account from serialized data. - * @param {WalletDB} data - * @param {Buffer} data - * @returns {Account} - */ - -Account.fromRaw = function fromRaw(db, data) { - return new Account(db).fromRaw(data); -}; - -/** - * Instantiate a Account from a - * jsonified account object. - * @param {WalletDB} db - * @param {Object} json - The jsonified account object. - * @returns {Account} - */ - -Account.fromJSON = function fromJSON(db, json) { - return new Account(db).fromJSON(json); -}; - -/** - * Test an object to see if it is a Account. - * @param {Object} obj - * @returns {Boolean} - */ - -Account.isAccount = function isAccount(obj) { - return obj - && typeof obj.receiveDepth === 'number' - && obj.deriveAddress === 'function'; -}; - /** * Master BIP32 key which can exist * in a timed out encrypted state. @@ -3400,20 +2577,8 @@ MasterKey.isMasterKey = function isMasterKey(obj) { && typeof obj.decrypt === 'function'; }; -/* - * Helpers - */ - -function cmp(key1, key2) { - return key1.compare(key2); -} - /* * Expose */ -exports = Wallet; -exports.Account = Account; -exports.MasterKey = MasterKey; - -module.exports = exports; +module.exports = Wallet; diff --git a/lib/bcoin/wallet/walletdb.js b/lib/bcoin/wallet/walletdb.js index bfb4eaa2..6c34b824 100644 --- a/lib/bcoin/wallet/walletdb.js +++ b/lib/bcoin/wallet/walletdb.js @@ -15,6 +15,7 @@ var constants = bcoin.constants; var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); var TXDB = require('./txdb'); +var Path = require('./path'); var MAX_POINT = String.fromCharCode(0xdbff, 0xdfff); // U+10FFFF /* @@ -1612,271 +1613,6 @@ WalletDB.prototype.getAddressPath = function getAddressPath(wid, hash, callback) }); }; -/** - * Path - * @constructor - * @private - * @property {WalletID} wid - * @property {String} name - Account name. - * @property {Number} account - Account index. - * @property {Number} change - Change index. - * @property {Number} index - Address index. - * @property {Address|null} address - */ - -function Path() { - if (!(this instanceof Path)) - return new Path(); - - this.wid = null; - this.name = null; - this.account = 0; - this.change = -1; - this.index = -1; - - this.encrypted = false; - this.imported = null; - this.script = null; - - // Currently unused. - this.type = bcoin.script.types.PUBKEYHASH; - this.version = -1; - - // Passed in by caller. - this.id = null; - this.hash = null; -} - -/** - * Clone the path object. - * @returns {Path} - */ - -Path.prototype.clone = function clone() { - var path = new Path(); - - path.wid = this.wid; - path.name = this.name; - path.account = this.account; - path.change = this.change; - path.index = this.index; - - path.encrypted = this.encrypted; - path.imported = this.imported; - path.script = this.script; - - path.type = this.type; - path.version = this.version; - - path.id = this.id; - path.hash = this.hash; - - return path; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -Path.prototype.fromRaw = function fromRaw(data) { - var p = new BufferReader(data); - - this.wid = p.readU32(); - this.name = p.readVarString('utf8'); - this.account = p.readU32(); - - switch (p.readU8()) { - case 0: - this.change = p.readU32(); - this.index = p.readU32(); - if (p.readU8() === 1) - this.script = p.readVarBytes(); - break; - case 1: - this.encrypted = p.readU8() === 1; - this.imported = p.readVarBytes(); - this.change = -1; - this.index = -1; - break; - default: - assert(false); - break; - } - - this.version = p.read8(); - this.type = p.readU8(); - - return this; -}; - -/** - * Instantiate path from serialized data. - * @param {Buffer} data - * @returns {Path} - */ - -Path.fromRaw = function fromRaw(data) { - return new Path().fromRaw(data); -}; - -/** - * Serialize path. - * @returns {Buffer} - */ - -Path.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - - p.writeU32(this.wid); - p.writeVarString(this.name, 'utf8'); - p.writeU32(this.account); - - if (this.index !== -1) { - assert(!this.imported); - p.writeU8(0); - p.writeU32(this.change); - p.writeU32(this.index); - if (this.script) { - p.writeU8(1); - p.writeVarBytes(this.script); - } else { - p.writeU8(0); - } - } else { - assert(this.imported); - p.writeU8(1); - p.writeU8(this.encrypted ? 1 : 0); - p.writeVarBytes(this.imported); - } - - p.write8(this.version); - p.writeU8(this.type); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from account. - * @private - * @param {WalletID} wid - * @param {KeyRing} ring - */ - -Path.prototype.fromAccount = function fromAccount(account, ring, change, index) { - this.wid = account.wid; - this.name = account.name; - this.account = account.accountIndex; - - if (change != null) - this.change = change; - - if (index != null) - this.index = index; - - 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 {KeyRing} ring - * @returns {Path} - */ - -Path.fromAccount = function fromAccount(account, ring, change, index) { - return new Path().fromAccount(account, ring, change, index); -}; - -/** - * Convert path object to string derivation path. - * @returns {String} - */ - -Path.prototype.toPath = function toPath() { - return 'm/' + this.account - + '\'/' + this.change - + '/' + this.index; -}; - -/** - * Convert path object to an address (currently unused). - * @returns {Address} - */ - -Path.prototype.toAddress = function toAddress(network) { - return bcoin.address.fromHash(this.hash, this.type, this.version, network); -}; - -/** - * Convert path to a json-friendly object. - * @returns {Object} - */ - -Path.prototype.toJSON = function toJSON() { - return { - name: this.name, - change: this.change === 1, - path: this.toPath() - }; -}; - -/** - * 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} - */ - -Path.prototype.inspect = function() { - return ''; -}; - /** * Path Info */ @@ -2023,7 +1759,7 @@ PathInfo.prototype.toDetails = function toDetails() { var details = this._details; if (!details) { - details = new TXDB.Details(this); + details = new Details(this); this._details = details; } @@ -2041,6 +1777,120 @@ PathInfo.prototype.toJSON = function toJSON() { return json; }; +/** + * Details + */ + +function Details(info) { + if (!(this instanceof Details)) + return new Details(info); + + this.db = info.db; + this.network = info.db.network; + this.wid = info.wid; + this.id = info.id; + this.hash = info.tx.hash('hex'); + this.height = info.tx.height; + this.block = info.tx.block; + this.index = info.tx.index; + this.confirmations = info.tx.getConfirmations(this.db.height); + this.fee = info.tx.getFee(); + this.ts = info.tx.ts; + this.ps = info.tx.ps; + this.tx = info.tx; + this.inputs = []; + this.outputs = []; + + this.init(info.table); +} + +Details.prototype.init = function init(table) { + this._insert(this.tx.inputs, this.inputs, table); + this._insert(this.tx.outputs, this.outputs, table); +}; + +Details.prototype._insert = function _insert(vector, target, table) { + var i, j, io, address, hash, paths, path, member; + + for (i = 0; i < vector.length; i++) { + io = vector[i]; + member = new DetailsMember(); + + if (io instanceof bcoin.input) + member.value = io.coin ? io.coin.value : 0; + else + member.value = io.value; + + address = io.getAddress(); + + if (address) { + member.address = address; + + hash = address.getHash('hex'); + paths = table[hash]; + + for (j = 0; j < paths.length; j++) { + path = paths[j]; + if (path.wid === this.wid) { + path.id = this.id; + member.path = path; + break; + } + } + } + + target.push(member); + } +}; + +Details.prototype.toJSON = function toJSON() { + var self = this; + return { + wid: this.wid, + id: this.id, + hash: utils.revHex(this.hash), + height: this.height, + block: this.block ? utils.revHex(this.block) : null, + ts: this.ts, + ps: this.ps, + index: this.index, + fee: utils.btc(this.fee), + confirmations: this.confirmations, + inputs: this.inputs.map(function(input) { + return input.toJSON(self.network); + }), + outputs: this.outputs.map(function(output) { + return output.toJSON(self.network); + }), + tx: this.tx.toRaw().toString('hex') + }; +}; + +/** + * DetailsMember + */ + +function DetailsMember() { + if (!(this instanceof DetailsMember)) + return new DetailsMember(); + + this.value = 0; + this.address = null; + this.path = null; +} + +DetailsMember.prototype.toJSON = function toJSON(network) { + return { + value: utils.btc(this.value), + address: this.address + ? this.address.toBase58(network) + : null, + path: this.path + ? this.path.toJSON() + : null + }; +}; + /* * Helpers */ diff --git a/lib/bcoin/workers/workers.js b/lib/bcoin/workers/workers.js index ead7403d..5fa24bc5 100644 --- a/lib/bcoin/workers/workers.js +++ b/lib/bcoin/workers/workers.js @@ -828,7 +828,7 @@ Framer.item = function _item(item, writer) { } else if (item instanceof bcoin.keyring) { p.writeU8(47); item.toRaw(p); - } else if (item instanceof bcoin.hd) { + } else if (bcoin.hd.isHD(item)) { p.writeU8(48); p.writeBytes(item.toRaw()); } else if (item instanceof bcoin.script) {