diff --git a/src/hdwallet.js b/src/hdwallet.js index 309955d..43207a1 100644 --- a/src/hdwallet.js +++ b/src/hdwallet.js @@ -13,7 +13,7 @@ var Network = require('./network') var HDWallet = module.exports = function(seed, network) { if (seed === undefined) return - var seedWords = convert.bytesToWordArray(convert.stringToBytes(seed)) + var seedWords = convert.bytesToWordArray(seed) var I = convert.wordArrayToBytes(HmacSHA512(seedWords, 'Bitcoin seed')) this.chaincode = I.slice(32) this.network = network || 'mainnet' @@ -36,9 +36,12 @@ function arrayEqual(a, b) { HDWallet.getChecksum = base58.getChecksum; -HDWallet.fromMasterHex = function(hex) { - var bytes = convert.hexToBytes(hex) - return new HDWallet(convert.bytesToString(bytes)) +HDWallet.fromSeedHex = function(hex, network) { + return new HDWallet(convert.hexToBytes(hex), network) +} + +HDWallet.fromSeedString = function(string, network) { + return new HDWallet(convert.stringToBytes(string), network) } HDWallet.fromBase58 = function(input) { diff --git a/src/wallet.js b/src/wallet.js index d203a54..698f9a3 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -6,7 +6,7 @@ var BigInteger = require('./jsbn/jsbn'); var Transaction = require('./transaction').Transaction; var TransactionIn = require('./transaction').TransactionIn; var TransactionOut = require('./transaction').TransactionOut; -var HDWallet = require('./hdwallet.js') +var HDNode = require('./hdwallet.js') var SecureRandom = require('./jsbn/rng'); var rng = new SecureRandom(); @@ -16,20 +16,16 @@ var Wallet = function (seed, options) { var options = options || {} var network = options.network || 'mainnet' - // HD first-level child derivation method (i.e. public or private child key derivation) - // NB: if not specified, defaults to private child derivation - // Also see https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 - this.derivationMethod = options.derivationMethod || 'private' - assert(this.derivationMethod == 'public' || this.derivationMethod == 'private', - "derivationMethod must be either 'public' or 'private'"); - // Stored in a closure to make accidental serialization less likely - var keys = []; var masterkey = null; var me = this; + var accountZero = null; + var internalAccount = null; + var externalAccount = null; // Addresses this.addresses = []; + this.changeAddresses = []; // Transaction output data this.outputs = {}; @@ -37,25 +33,37 @@ var Wallet = function (seed, options) { // Make a new master key this.newMasterKey = function(seed, network) { if (!seed) { - var seedBytes = new Array(32); - rng.nextBytes(seedBytes); - seed = convert.bytesToString(seedBytes) + var seed= new Array(32); + rng.nextBytes(seed); } - masterkey = new HDWallet(seed, network); - keys = [] + masterkey = new HDNode(seed, network); + + // HD first-level child derivation method should be private + // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 + accountZero = masterkey.derivePrivate(0) + externalAccount = accountZero.derive(0) + internalAccount = accountZero.derive(1) + + me.addresses = []; + me.changeAddresses = []; + + me.outputs = {}; } this.newMasterKey(seed, network) - // Add a new address + this.generateAddress = function() { - if(this.derivationMethod == 'private') - keys.push(masterkey.derivePrivate(keys.length)); - else - keys.push(masterkey.derive(keys.length)); - this.addresses.push(keys[keys.length-1].getBitcoinAddress().toString()) + var key = externalAccount.derive(this.addresses.length) + this.addresses.push(key.getBitcoinAddress().toString()) return this.addresses[this.addresses.length - 1] } + this.generateChangeAddress = function() { + var key = internalAccount.derive(this.changeAddresses.length) + this.changeAddresses.push(key.getBitcoinAddress().toString()) + return this.changeAddresses[this.changeAddresses.length - 1] + } + // Processes a transaction object // If "verified" is true, then we trust the transaction as "final" this.processTx = function(tx, verified) { @@ -167,27 +175,36 @@ var Wallet = function (seed, options) { tx.ins.map(function(inp,i) { var inp = inp.outpoint.hash+':'+inp.outpoint.index; if (me.outputs[inp]) { - var address = me.outputs[inp].address, - ind = me.addresses.indexOf(address); - if (ind >= 0) { - var key = keys[ind] - tx.sign(ind,key) - } + var address = me.outputs[inp].address + tx.sign(i, me.getPrivateKeyForAddress(address)) } }) return tx; } this.getMasterKey = function() { return masterkey } + this.getAccountZero = function() { return accountZero } + this.getInternalAccount = function() { return internalAccount } + this.getExternalAccount = function() { return externalAccount } this.getPrivateKey = function(index) { - if (typeof index == "string") - return keys.filter(function(i,k){ return addresses[i] == index })[0] - else - return keys[index] + return externalAccount.derive(index).priv } - this.getPrivateKeys = function() { return keys } + this.getInternalPrivateKey = function(index) { + return internalAccount.derive(index).priv + } + + this.getPrivateKeyForAddress = function(address) { + var index; + if((index = this.addresses.indexOf(address)) > -1) { + return this.getPrivateKey(index) + } else if((index = this.changeAddresses.indexOf(address)) > -1) { + return this.getInternalPrivateKey(index) + } else { + throw new Error('Unknown address. Make sure the address is from the keychain and has been generated.') + } + } }; module.exports = Wallet; diff --git a/test/hdwallet.js b/test/hdwallet.js index 85d2ae4..f637535 100644 --- a/test/hdwallet.js +++ b/test/hdwallet.js @@ -32,19 +32,46 @@ describe('HDWallet', function() { }) }) - describe('ctor', function() { - it('creates from seed', function() { - var seed = 'crazy horse battery staple' - , hd = new HDWallet(seed) + describe('constructor & seed deserialization', function() { + var expectedPrivKey, seed; - assert(hd.priv) + beforeEach(function(){ + expectedPrivKey = 'KwkW62Lzm4a7Eo5nPLezrVjWBGFh2KMfpyf4Swz9NmfsVaLoeXv9' + seed = [ + 99, 114, 97, 122, 121, 32, 104, 111, 114, 115, 101, 32, 98, + 97, 116, 116, 101, 114, 121, 32, 115, 116, 97, 112, 108, 101 + ] + }) + + it('creates from binary seed', function() { + var hd = new HDWallet(seed) + + assert.equal(hd.priv, expectedPrivKey) assert(hd.pub) }) + + describe('fromSeedHex', function() { + it('creates from hex seed', function() { + var hd = HDWallet.fromSeedHex(b2h(seed)) + + assert.equal(hd.priv, expectedPrivKey) + assert(hd.pub) + }) + }) + + describe('fromSeedString', function() { + it('creates from string seed', function() { + var hd = HDWallet.fromSeedString(convert.bytesToString(seed)) + + assert.equal(hd.priv, expectedPrivKey) + assert(hd.pub) + }) + }) }) describe('Test vectors', function() { it('Test vector 1', function() { - var hd = HDWallet.fromMasterHex('000102030405060708090a0b0c0d0e0f') + var hd = HDWallet.fromSeedHex('000102030405060708090a0b0c0d0e0f') // m assert.equal(b2h(hd.getIdentifier()), '3442193e1bb70916e914552172cd4e2dbc9df811') @@ -131,7 +158,7 @@ describe('HDWallet', function() { }) it('Test vector 2', function() { - var hd = HDWallet.fromMasterHex('fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542') + var hd = HDWallet.fromSeedHex('fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542') // m assert.equal(b2h(hd.getIdentifier()), 'bd16bee53961a47d6ad888e29545434a89bdfe95') diff --git a/test/wallet.js b/test/wallet.js index 650ad5c..b72e2d7 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,10 +1,17 @@ var Wallet = require('../src/wallet.js') +var HDNode = require('../src/hdwallet.js') +var convert = require('../src/convert.js') var assert = require('assert') +var SHA256 = require('crypto-js/sha256') +var Crypto = require('crypto-js') describe('Wallet', function() { - var seed = 'crazy horse battery staple' + var seed; + beforeEach(function(){ + seed = convert.wordArrayToBytes(SHA256("don't use a string seed like this in real life")) + }) - describe('default constructor', function() { + describe('constructor', function() { var wallet; beforeEach(function() { wallet = new Wallet(seed) @@ -14,24 +21,139 @@ describe('Wallet', function() { assert.equal(wallet.getMasterKey().network, 'mainnet') }) - it('defaults to private derivationMethod', function() { - assert.equal(wallet.derivationMethod, 'private') + it("generates m/0' as the main account", function() { + var mainAccount = wallet.getAccountZero() + assert.equal(mainAccount.index, 0 + HDNode.HIGHEST_BIT) + assert.equal(mainAccount.depth, 1) + }) + + it("generates m/0'/0 as the external account", function() { + var account = wallet.getExternalAccount() + assert.equal(account.index, 0) + assert.equal(account.depth, 2) + }) + + it("generates m/0'/1 as the internal account", function() { + var account = wallet.getInternalAccount() + assert.equal(account.index, 1) + assert.equal(account.depth, 2) + }) + + describe('when seed is not specified', function(){ + it('generates a seed', function(){ + var wallet = new Wallet() + assert.ok(wallet.getMasterKey()) + }) + }) + + describe('constructor options', function() { + var wallet; + beforeEach(function() { + wallet = new Wallet(seed, {network: 'testnet'}) + }) + + it('uses the network if specified', function() { + assert.equal(wallet.getMasterKey().network, 'testnet') + }) }) }) - describe('constructor options', function() { - var wallet; - beforeEach(function() { - wallet = new Wallet(seed, {network: 'testnet', derivationMethod: 'public'}) + describe('newMasterKey', function(){ + it('resets accounts', function(){ + var wallet = new Wallet() + var oldAccountZero = wallet.getAccountZero() + var oldExternalAccount = wallet.getExternalAccount() + var oldInternalAccount = wallet.getInternalAccount() + + wallet.newMasterKey(seed) + assertNotEqual(wallet.getAccountZero(), oldAccountZero) + assertNotEqual(wallet.getExternalAccount(), oldExternalAccount) + assertNotEqual(wallet.getInternalAccount(), oldInternalAccount) }) - it('uses the network if specified', function() { - assert.equal(wallet.getMasterKey().network, 'testnet') + it('resets addresses', function(){ + var wallet = new Wallet() + wallet.generateAddress() + wallet.generateChangeAddress() + var oldAddresses = wallet.addresses + var oldChangeAddresses = wallet.changeAddresses + assert.notDeepEqual(oldAddresses, []) + assert.notDeepEqual(oldChangeAddresses, []) + + wallet.newMasterKey(seed) + assert.deepEqual(wallet.addresses, []) + assert.deepEqual(wallet.changeAddresses, []) }) + }) + + describe('generateAddress', function(){ + it('generate receiving addresses', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + var expectedAddresses = [ + "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", + "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X" + ] - it('uses the derivationMethod if specified', function() { - assert.equal(wallet.derivationMethod, 'public') + assert.equal(wallet.generateAddress(), expectedAddresses[0]) + assert.equal(wallet.generateAddress(), expectedAddresses[1]) + assert.deepEqual(wallet.addresses, expectedAddresses) }) }) + describe('generateChangeAddress', function(){ + it('generates change addresses', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"] + + assert.equal(wallet.generateChangeAddress(), expectedAddresses[0]) + assert.deepEqual(wallet.changeAddresses, expectedAddresses) + }) + }) + + describe('getPrivateKey', function(){ + it('returns the private key at the given index of external account', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + + assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).priv) + assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).priv) + }) + }) + + describe('getInternalPrivateKey', function(){ + it('returns the private key at the given index of internal account', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + + assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).priv) + assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).priv) + }) + }) + + describe('getPrivateKeyForAddress', function(){ + it('returns the private key for the given address', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + wallet.generateChangeAddress() + wallet.generateAddress() + wallet.generateAddress() + + assertEqual(wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"), + wallet.getExternalAccount().derive(1).priv) + assertEqual(wallet.getPrivateKeyForAddress("mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"), + wallet.getInternalAccount().derive(0).priv) + }) + + it('raises an error when address is not found', function(){ + var wallet = new Wallet(seed, {network: 'testnet'}) + assert.throws(function() { + wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X") + }, Error, 'Unknown address. Make sure the address is from the keychain and has been generated.') + }) + }) + + function assertEqual(obj1, obj2){ + assert.equal(obj1.toString(), obj2.toString()) + } + + function assertNotEqual(obj1, obj2){ + assert.notEqual(obj1.toString(), obj2.toString()) + } })