From f44e662dbe19fac35d8cbd9d8fa09d8d3682664a Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sun, 4 Mar 2018 20:48:55 +0100 Subject: [PATCH] allow custom bip32 prefixes, and varying types of addresses returned get hdnode.getAddress --- src/hdnode.js | 56 ++++++++++++++------ src/keytoscript.js | 124 +++++++++++++++++++++++++++++++++++++++++++++ src/networks.js | 23 +++++++-- test/hdaddress.js | 95 ++++++++++++++++++++++++++++++++++ test/hdnode.js | 13 +++-- 5 files changed, 286 insertions(+), 25 deletions(-) create mode 100644 src/keytoscript.js create mode 100644 test/hdaddress.js diff --git a/src/hdnode.js b/src/hdnode.js index a96b20c..8f98846 100644 --- a/src/hdnode.js +++ b/src/hdnode.js @@ -1,6 +1,7 @@ var Buffer = require('safe-buffer').Buffer var base58check = require('bs58check') var bcrypto = require('./crypto') +var baddress = require('./address') var createHmac = require('create-hmac') var typeforce = require('typeforce') var types = require('./types') @@ -9,12 +10,15 @@ var NETWORKS = require('./networks') var BigInteger = require('bigi') var ECPair = require('./ecpair') +// var KeyToScript = require('./keytoscript') var ecurve = require('ecurve') var curve = ecurve.getCurveByName('secp256k1') -function HDNode (keyPair, chainCode) { +function HDNode (keyPair, chainCode, prefix) { typeforce(types.tuple('ECPair', types.Buffer256bit), arguments) - + if (!prefix) { + prefix = keyPair.network.bip32 + } if (!keyPair.compressed) throw new TypeError('BIP32 only allows compressed keyPairs') this.keyPair = keyPair @@ -22,13 +26,14 @@ function HDNode (keyPair, chainCode) { this.depth = 0 this.index = 0 this.parentFingerprint = 0x00000000 + this.prefix = prefix } HDNode.HIGHEST_BIT = 0x80000000 HDNode.LENGTH = 78 HDNode.MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8') -HDNode.fromSeedBuffer = function (seed, network) { +HDNode.fromSeedBuffer = function (seed, network, prefix) { typeforce(types.tuple(types.Buffer, types.maybe(types.Network)), arguments) if (seed.length < 16) throw new TypeError('Seed should be at least 128 bits') @@ -45,14 +50,21 @@ HDNode.fromSeedBuffer = function (seed, network) { network: network }) - return new HDNode(keyPair, IR) + return new HDNode(keyPair, IR, prefix) } -HDNode.fromSeedHex = function (hex, network) { - return HDNode.fromSeedBuffer(Buffer.from(hex, 'hex'), network) +HDNode.fromSeedHex = function (hex, network, prefix) { + return HDNode.fromSeedBuffer(Buffer.from(hex, 'hex'), network, prefix) } -HDNode.fromBase58 = function (string, networks) { +/** + * + * @param {string} string + * @param {Array|network} networks + * @param {Array} prefixes - only used if networks is object/undefined(so bitcoin). + * @returns {HDNode} + */ +HDNode.fromBase58 = function (string, networks, prefixes) { var buffer = base58check.decode(string) if (buffer.length !== 78) throw new Error('Invalid buffer length') @@ -61,6 +73,7 @@ HDNode.fromBase58 = function (string, networks) { var network // list of networks? + var prefix if (Array.isArray(networks)) { network = networks.filter(function (x) { return version === x.bip32.private || @@ -69,13 +82,26 @@ HDNode.fromBase58 = function (string, networks) { if (!network) throw new Error('Unknown network version') + // we found a network by it's bip32 prefixes, use that + prefix = network.bip32 + // otherwise, assume a network object (or default to bitcoin) } else { network = networks || NETWORKS.bitcoin + if (prefixes) { + prefix = prefixes.filter(function (x) { + return version === x.private || + version === x.public + }).pop() + } else { + // no special prefixes to consider, use networks bip32 prefix + prefix = network.bip32 + } } - if (version !== network.bip32.private && - version !== network.bip32.public) throw new Error('Invalid network version') + // sanity check the version against the prefix + if (version !== prefix.private && + version !== prefix.public) throw new Error('Invalid network version') // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, ... var depth = buffer[4] @@ -114,7 +140,7 @@ HDNode.fromBase58 = function (string, networks) { keyPair = new ECPair(null, Q, { network: network }) } - var hd = new HDNode(keyPair, chainCode) + var hd = new HDNode(keyPair, chainCode, prefix) hd.depth = depth hd.index = index hd.parentFingerprint = parentFingerprint @@ -123,7 +149,8 @@ HDNode.fromBase58 = function (string, networks) { } HDNode.prototype.getAddress = function () { - return this.keyPair.getAddress() + var data = this.prefix.scriptFactory.convert(this.keyPair) + return baddress.fromOutputScript(data.scriptPubKey, this.keyPair.network) } HDNode.prototype.getIdentifier = function () { @@ -147,7 +174,7 @@ HDNode.prototype.neutered = function () { network: this.keyPair.network }) - var neutered = new HDNode(neuteredKeyPair, this.chainCode) + var neutered = new HDNode(neuteredKeyPair, this.chainCode, this.prefix) neutered.depth = this.depth neutered.index = this.index neutered.parentFingerprint = this.parentFingerprint @@ -167,8 +194,7 @@ HDNode.prototype.toBase58 = function (__isPrivate) { if (__isPrivate !== undefined) throw new TypeError('Unsupported argument in 2.0.0') // Version - var network = this.keyPair.network - var version = (!this.isNeutered()) ? network.bip32.private : network.bip32.public + var version = (!this.isNeutered()) ? this.prefix.private : this.prefix.public var buffer = Buffer.allocUnsafe(78) // 4 bytes: version bytes @@ -268,7 +294,7 @@ HDNode.prototype.derive = function (index) { }) } - var hd = new HDNode(derivedKeyPair, IR) + var hd = new HDNode(derivedKeyPair, IR, this.prefix) hd.depth = this.depth + 1 hd.index = index hd.parentFingerprint = this.getFingerprint().readUInt32BE(0) diff --git a/src/keytoscript.js b/src/keytoscript.js new file mode 100644 index 0000000..c16272d --- /dev/null +++ b/src/keytoscript.js @@ -0,0 +1,124 @@ +var bcrypto = require('./crypto') +var btemplates = require('./templates') + +function checkAllowedP2sh (keyFactory) { + if (!(keyFactory instanceof P2pkhFactory || + keyFactory instanceof P2wpkhFactory || + keyFactory instanceof P2pkFactory + )) { + throw new Error('Unsupported script factory for P2SH') + } +} + +function checkAllowedP2wsh (keyFactory) { + if (!(keyFactory instanceof P2pkhFactory || + keyFactory instanceof P2pkFactory + )) { + throw new Error('Unsupported script factory for P2SH') + } +} + +var P2pkFactory = function () { + +} + +/** + * @param {bitcoin.ECPair} key + */ +P2pkFactory.prototype.convert = function (key) { + return { + scriptPubKey: btemplates.pubKey.output.encode(key.getPublicKeyBuffer()), + signData: {} + } +} + +var P2pkhFactory = function () { + +} + +/** + * @param {bitcoin.ECPair} key + */ +P2pkhFactory.prototype.convert = function (key) { + var hash160 = bcrypto.hash160(key.getPublicKeyBuffer()) + return { + scriptPubKey: btemplates.pubKeyHash.output.encode(hash160), + signData: {} + } +} + +var P2wpkhFactory = function () { + +} + +/** + * @param {bitcoin.ECPair} key + */ +P2wpkhFactory.prototype.convert = function (key) { + var hash160 = bcrypto.hash160(key.getPublicKeyBuffer()) + return { + scriptPubKey: btemplates.witnessPubKeyHash.output.encode(hash160), + signData: {} + } +} + +var P2shFactory = function (keyFactory) { + checkAllowedP2sh(keyFactory) + this.factory = keyFactory +} + +P2shFactory.prototype.convert = function (key) { + var detail = this.factory.convert(key) + var hash160 = bcrypto.hash160(detail.scriptPubKey) + return { + scriptPubKey: btemplates.scriptHash.output.encode(hash160), + signData: { + redeemScript: detail.scriptPubKey + } + } +} + +var P2wshFactory = function (keyFactory) { + checkAllowedP2wsh(keyFactory) + this.factory = keyFactory +} + +P2wshFactory.prototype.convert = function (key) { + var detail = this.factory.convert(key) + var hash160 = bcrypto.hash160(detail.scriptPubKey) + return { + scriptPubKey: btemplates.scriptHash.output.encode(hash160), + signData: { + redeemScript: detail.scriptPubKey + } + } +} + +var P2shP2wshFactory = function (keyFactory) { + checkAllowedP2wsh(keyFactory) + this.factory = keyFactory +} + +P2shP2wshFactory.prototype.convert = function (key) { + var detail = this.factory.convert(key) + var sha256 = bcrypto.sha256(detail.scriptPubKey) + var wp = btemplates.witnessScriptHash.output.encode(sha256) + var hash160 = bcrypto.hash160(wp) + var spk = btemplates.scriptHash.output.encode(hash160) + return { + scriptPubKey: spk, + signData: { + redeemScript: wp, + witnessScript: detail.scriptPubKey + } + } +} + +module.exports = { + P2pkhFactory: P2pkhFactory, + P2wpkhFactory: P2wpkhFactory, + P2pkFactory: P2pkFactory, + P2shFactory: P2shFactory, + P2wshFactory: P2wshFactory, + P2shP2wshFactory: P2shP2wshFactory +} diff --git a/src/networks.js b/src/networks.js index 43cd5fc..bfef034 100644 --- a/src/networks.js +++ b/src/networks.js @@ -1,13 +1,28 @@ +var KeyToScript = require('./keytoscript') // https://en.bitcoin.it/wiki/List_of_address_prefixes // Dogecoin BIP32 is a proposed standard: https://bitcointalk.org/index.php?topic=409731 +var p2pkh = new KeyToScript.P2pkhFactory() +var p2wpkh = new KeyToScript.P2wpkhFactory() + module.exports = { bitcoin: { messagePrefix: '\x18Bitcoin Signed Message:\n', bech32: 'bc', bip32: { public: 0x0488b21e, - private: 0x0488ade4 + private: 0x0488ade4, + scriptFactory: p2pkh + }, + bip49: { + private: 0x049d7878, + public: 0x049d7cb2, + scriptFactory: new KeyToScript.P2shFactory(p2wpkh) + }, + bip84: { + private: 0x04b2430c, + public: 0x04b24746, + scriptFactory: p2wpkh }, pubKeyHash: 0x00, scriptHash: 0x05, @@ -18,7 +33,8 @@ module.exports = { bech32: 'tb', bip32: { public: 0x043587cf, - private: 0x04358394 + private: 0x04358394, + scriptFactory: p2pkh }, pubKeyHash: 0x6f, scriptHash: 0xc4, @@ -28,7 +44,8 @@ module.exports = { messagePrefix: '\x19Litecoin Signed Message:\n', bip32: { public: 0x019da462, - private: 0x019d9cfe + private: 0x019d9cfe, + scriptFactory: p2pkh }, pubKeyHash: 0x30, scriptHash: 0x32, diff --git a/test/hdaddress.js b/test/hdaddress.js new file mode 100644 index 0000000..59b84ab --- /dev/null +++ b/test/hdaddress.js @@ -0,0 +1,95 @@ +/* global describe, it */ +/* eslint-disable no-new */ + +var assert = require('assert') +var HDNode = require('../src/hdnode') +var bnetwork = require('../src/networks') + +describe('bip44', function () { + it('works without a ScriptFactory', function () { + // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about + var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4' + var network = bnetwork.bitcoin + + var root = HDNode.fromSeedHex(seed, network) + assert.equal( + root.toBase58(), + 'xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu' + ) + + var account = root.derivePath('44\'/0\'/0\'') + assert.equal( + account.toBase58(), + 'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb' + ) + assert.equal( + 'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj', + account.neutered().toBase58() + ) + + var key = account.derivePath('0/0') + assert.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', key.getAddress()) + }) + + it('works with a prefix', function () { + // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about + var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4' + var network = bnetwork.bitcoin + + var root = HDNode.fromSeedHex(seed, network, network.bip32) + var account = root.derivePath('44\'/0\'/0\'') + assert.equal( + account.toBase58(), + 'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb' + ) + assert.equal( + 'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj', + account.neutered().toBase58() + ) + + var key = account.derivePath('0/0') + assert.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', key.getAddress()) + }) +}) + +describe('bip49', function () { + it('serialization and address', function () { + // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about + var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4' + var network = bnetwork.bitcoin + var root = HDNode.fromSeedHex(seed, network, network.bip49) + var account = root.derivePath('49\'/0\'/0\'') + assert.equal( + account.toBase58(), + 'yprvAHwhK6RbpuS3dgCYHM5jc2ZvEKd7Bi61u9FVhYMpgMSuZS613T1xxQeKTffhrHY79hZ5PsskBjcc6C2V7DrnsMsNaGDaWev3GLRQRgV7hxF' + ) + assert.equal( + 'ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP', + account.neutered().toBase58() + ) + + var key = account.derivePath('0/0') + assert.equal('37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf', key.getAddress()) + }) +}) + +describe('bip84', function () { + it('serialization and address', function () { + // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about + var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4' + var network = bnetwork.bitcoin + var root = HDNode.fromSeedHex(seed, network, network.bip84) + var account = root.derivePath('84\'/0\'/0\'') + assert.equal( + account.toBase58(), + 'zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE' + ) + assert.equal( + 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs', + account.neutered().toBase58() + ) + + var key = account.derivePath('0/0') + assert.equal('bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', key.getAddress()) + }) +}) diff --git a/test/hdnode.js b/test/hdnode.js index 944dded..23cd042 100644 --- a/test/hdnode.js +++ b/test/hdnode.js @@ -50,7 +50,6 @@ describe('HDNode', function () { it('has a default depth/index of 0', function () { var hd = new HDNode(keyPair, chainCode) - assert.strictEqual(hd.depth, 0) assert.strictEqual(hd.index, 0) }) @@ -124,12 +123,12 @@ describe('HDNode', function () { }) describe('getAddress', function () { - it('wraps keyPair.getAddress', setupTest(function () { - this.mock(keyPair).expects('getAddress') - .once().withArgs().returns('foobar') - - assert.strictEqual(hd.getAddress(), 'foobar') - })) + // it('wraps keyPair.getAddress', setupTest(function () { + // this.mock(keyPair).expects('getAddress') + // .once().withArgs().returns('foobar') + // + // assert.strictEqual(hd.getAddress(), 'foobar') + // })) }) describe('getNetwork', function () {