diff --git a/.jshintrc b/.jshintrc index e7d7b7a..8be24f2 100644 --- a/.jshintrc +++ b/.jshintrc @@ -30,15 +30,15 @@ "maxlen": 600, // Maximum number of lines of code in a file "predef": [ // Extra globals. - "define", - "require", - "exports", - "module", - "describe", - "before", - "beforeEach", "after", "afterEach", - "it" + "before", + "beforeEach", + "define", + "describe", + "exports", + "it", + "module", + "require" ] } diff --git a/gulpfile.js b/gulpfile.js index 6285ae7..8d71431 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -60,6 +60,12 @@ gulp.task('watch:test', function() { return gulp.watch(alljs, ['test-nofail']); }); +gulp.task('watch:coverage', function() { + // TODO: Only run tests that are linked to file changes by doing + // something smart like reading through the require statements + return gulp.watch(alljs, ['coverage']); +}); + gulp.task('watch:lint', function() { // TODO: Only lint files that are linked to file changes by doing // something smart like reading through the require statements diff --git a/index.js b/index.js index ec211dd..17e3d9f 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,8 @@ bitcore.encoding.BufferReader = require('./lib/encoding/bufferreader'); bitcore.encoding.BufferWriter = require('./lib/encoding/bufferwriter'); bitcore.encoding.Varint = require('./lib/encoding/varint'); +bitcore.util = require('./lib/util'); + // main bitcoin library bitcore.Address = require('./lib/address'); bitcore.Block = require('./lib/block'); diff --git a/lib/hdprivatekey.js b/lib/hdprivatekey.js index cd29b58..f7c1955 100644 --- a/lib/hdprivatekey.js +++ b/lib/hdprivatekey.js @@ -31,20 +31,20 @@ function HDPrivateKey(arg) { if (_.isString(arg) || buffer.Buffer.isBuffer(arg)) { if (HDPrivateKey.isValidSerialized(arg)) { this._buildFromSerialized(arg); + } else if (util.isValidJson(arg)) { + this._buildFromJson(arg); } else { throw new Error(HDPrivateKey.getSerializedError(arg)); } } else { if (_.isObject(arg)) { this._buildFromObject(arg); - } else if (util.isValidJson(arg)) { - this._buildFromJson(arg); } else { throw new Error(HDPrivateKey.Errors.UnrecognizedArgument); } } } else { - this._generateRandomly(); + return this._generateRandomly(); } } @@ -162,8 +162,8 @@ HDPrivateKey._validateNetwork = function(data, network) { if (!network) { return HDPrivateKey.Errors.InvalidNetworkArgument; } - var version = data.slice(4); - if (version.toString() !== network.xprivkey.toString()) { + var version = data.slice(0, 4); + if (util.integerFromBuffer(version) !== network.xprivkey) { return HDPrivateKey.Errors.InvalidNetwork; } return null; @@ -176,13 +176,13 @@ HDPrivateKey.prototype._buildFromJson = function(arg) { HDPrivateKey.prototype._buildFromObject = function(arg) { // TODO: Type validation var buffers = { - version: util.integerAsBuffer(Network.get(arg.network).xprivkey), + version: arg.network ? util.integerAsBuffer(Network.get(arg.network).xprivkey) : arg.version, depth: util.integerAsSingleByteBuffer(arg.depth), parentFingerPrint: _.isNumber(arg.parentFingerPrint) ? util.integerAsBuffer(arg.parentFingerPrint) : arg.parentFingerPrint, childIndex: _.isNumber(arg.childIndex) ? util.integerAsBuffer(arg.childIndex) : arg.childIndex, chainCode: _.isString(arg.chainCode) ? util.hexToBuffer(arg.chainCode) : arg.chainCode, - privateKey: _.isString(arg.privateKey) ? util.hexToBuffer(arg.privateKey) : arg.privateKey, - checksum: arg.checksum && arg.checksum.length ? util.integerAsBuffer(arg.checksum) : undefined + privateKey: (_.isString(arg.privateKey) && util.isHexa(arg.privateKey)) ? util.hexToBuffer(arg.privateKey) : arg.privateKey, + checksum: arg.checksum ? (arg.checksum.length ? arg.checksum : util.integerAsBuffer(arg.checksum)) : undefined }; return this._buildFromBuffers(buffers); }; @@ -214,13 +214,13 @@ HDPrivateKey.fromSeed = function(hexa, network) { hexa = util.hexToBuffer(hexa); } if (!Buffer.isBuffer(hexa)) { - throw new Error(HDPrivateKey.InvalidEntropyArg); + throw new Error(HDPrivateKey.Errors.InvalidEntropyArg); } if (hexa.length < MINIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new Error(HDPrivateKey.NotEnoughEntropy); + throw new Error(HDPrivateKey.Errors.NotEnoughEntropy); } if (hexa.length > MAXIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new Error('More than 512 bytes of entropy is nonstandard'); + throw new Error(HDPrivateKey.Errors.TooMuchEntropy); } var hash = Hash.sha512hmac(hexa, new buffer.Buffer('Bitcoin seed')); @@ -264,7 +264,7 @@ HDPrivateKey.prototype._buildFromBuffers = function(arg) { if (!arg.checksum || !arg.checksum.length) { arg.checksum = Base58Check.checksum(buffer.Buffer.concat(sequence)); } else { - if (arg.checksum.toString() !== sequence.toString()) { + if (arg.checksum.toString() !== Base58Check.checksum(buffer.Buffer.concat(sequence)).toString()) { throw new Error(HDPrivateKey.Errors.InvalidB58Checksum); } } @@ -316,11 +316,11 @@ HDPrivateKey.prototype.toObject = function() { return { network: Network.get(util.integerFromBuffer(this._buffers.version)).name, depth: util.integerFromSingleByteBuffer(this._buffers.depth), - fingerPrint: this.fingerPrint, + fingerPrint: util.integerFromBuffer(this.fingerPrint), parentFingerPrint: util.integerFromBuffer(this._buffers.parentFingerPrint), childIndex: util.integerFromBuffer(this._buffers.childIndex), chainCode: util.bufferToHex(this._buffers.chainCode), - privateKey: this.privateKey.toString(), + privateKey: this.privateKey.toBuffer().toString('hex'), checksum: util.integerFromBuffer(this._buffers.checksum), xprivkey: this.xprivkey }; diff --git a/lib/util.js b/lib/util.js index 0651891..d4e8af4 100644 --- a/lib/util.js +++ b/lib/util.js @@ -11,7 +11,18 @@ var isHexa = function isHexa(value) { return /^[0-9a-fA-F]+$/.test(value); }; +var shallowEquals = function(obj1, obj2) { + var keys1 = _.keys(obj1); + var keys2 = _.keys(obj2); + if (_.size(keys1) !== _.size(keys2)) { + return false; + } + var compare = function(key) { return obj1[key] === obj2[key]; }; + return _.all(keys1, compare) && _.all(keys2, compare); +}; + module.exports = { + shallowEquals: shallowEquals, isValidJson: function isValidJson(arg) { try { JSON.parse(arg); diff --git a/test/hdprivatekey.js b/test/hdprivatekey.js index 5f69d34..8b233d6 100644 --- a/test/hdprivatekey.js +++ b/test/hdprivatekey.js @@ -1,15 +1,56 @@ 'use strict'; /* jshint unused: false */ +var _ = require('lodash'); +var assert = require('assert'); var should = require('chai').should(); +var expect = require('chai').expect; var bitcore = require('..'); +var buffer = require('buffer'); +var util = bitcore.util; var HDPrivateKey = bitcore.HDPrivateKey; +var Base58Check = bitcore.encoding.Base58Check; var xprivkey = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; +var json = '{"network":"livenet","depth":0,"fingerPrint":876747070,"parentFingerPrint":0,"childIndex":0,"chainCode":"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508","privateKey":"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35","checksum":-411132559,"xprivkey":"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}'; describe('HDPrivate key interface', function() { + /* jshint maxstatements: 50 */ + var expectFail = function(argument, error) { + expect(function() { + var privateKey = new HDPrivateKey(argument); + }).to.throw(error); + }; + var expectDerivationFail = function(argument, error) { + expect(function() { + var privateKey = new HDPrivateKey(xprivkey); + privateKey.derive(argument); + }).to.throw(error); + }; it('should make a new private key from random', function() { - new HDPrivateKey().should.exist(); + (new HDPrivateKey().xprivkey).should.exist(); + }); + + it('should error with an invalid checksum', function() { + expectFail(xprivkey + '1', HDPrivateKey.Errors.InvalidB58Checksum); + }); + + it('can be rebuilt from a json generated by itself', function() { + var regenerate = new HDPrivateKey(json); + regenerate.xprivkey.should.equal(xprivkey); + }); + + it('builds a json keeping the same interface than previous versions', function() { + assert(util.shallowEquals( + JSON.parse(new HDPrivateKey(json).toJson()), + JSON.parse(new HDPrivateKey(xprivkey).toJson()) + )); + }); + + describe('should error with a nonsensical argument', function() { + it('like a number', function() { + expectFail(1, HDPrivateKey.Errors.UnrecognizedArgument); + }); }); it('allows no-new calling', function() { @@ -21,11 +62,91 @@ describe('HDPrivate key interface', function() { .xprivkey.should.equal(xprivkey); }); + it('fails when trying to derive with an invalid argument', function() { + expectDerivationFail([], HDPrivateKey.Errors.InvalidDerivationArgument); + }); + + it('catches early invalid paths', function() { + expectDerivationFail('s', HDPrivateKey.Errors.InvalidPath); + }); + + it('allows derivation of hardened keys by passing a very big number', function() { + var privateKey = new HDPrivateKey(xprivkey); + var derivedByNumber = privateKey.derive(0x80000000); + var derivedByArgument = privateKey.derive(0, true); + derivedByNumber.xprivkey.should.equal(derivedByArgument.xprivkey); + }); + + it('returns itself with "m" parameter', function() { + var privateKey = new HDPrivateKey(xprivkey); + privateKey.should.equal(privateKey.derive('m')); + }); + + it('returns InvalidArgument if invalid data is given to getSerializedError', function() { + HDPrivateKey.getSerializedError(1).should.equal(HDPrivateKey.Errors.InvalidArgument); + }); + + it('returns InvalidLength if data of invalid length is given to getSerializedError', function() { + HDPrivateKey.getSerializedError(Base58Check.encode(new buffer.Buffer('onestring'))).should.equal(HDPrivateKey.Errors.InvalidLength); + }); + + it('returns InvalidNetworkArgument if an invalid network is provided', function() { + HDPrivateKey.getSerializedError(xprivkey, 'invalidNetwork').should.equal(HDPrivateKey.Errors.InvalidNetworkArgument); + }); + + it('recognizes that the wrong network was asked for', function() { + HDPrivateKey.getSerializedError(xprivkey, 'testnet').should.equal(HDPrivateKey.Errors.InvalidNetwork); + }); + + it('recognizes the correct network', function() { + expect(HDPrivateKey.getSerializedError(xprivkey, 'livenet')).to.equal(null); + }); + + describe('on creation from seed', function() { + var expectSeedFail = function(argument, error) { + expect(function() { + return HDPrivateKey.fromSeed(argument); + }).to.throw(error); + }; + it('converts correctly from an hexa string', function() { + HDPrivateKey.fromSeed('01234567890abcdef01234567890abcdef').xprivkey.should.exist(); + }); + it('fails when argument is not a buffer or string', function() { + expectSeedFail(1, HDPrivateKey.Errors.InvalidEntropyArg); + }); + it('fails when argument doesn\'t provide enough entropy', function() { + expectSeedFail('01', HDPrivateKey.Errors.NotEnoughEntropy); + }); + it('fails when argument provides too much entropy', function() { + var entropy = '0'; + for (var i = 0; i < 129; i++) { + entropy += '1'; + } + expectSeedFail(entropy, HDPrivateKey.Errors.TooMuchEntropy); + }); + }); + + it('correctly errors if an invalid checksum is provided', function() { + var privKey = new HDPrivateKey(xprivkey); + expect(function() { + var buffers = privKey._buffers; + buffers.checksum = util.integerAsBuffer(0); + return new HDPrivateKey(buffers); + }).to.throw(HDPrivateKey.Errors.InvalidB58Checksum); + }); + it('correctly validates the checksum', function() { + var privKey = new HDPrivateKey(xprivkey); + expect(function() { + var buffers = privKey._buffers; + return new HDPrivateKey(buffers); + }).to.not.throw(); + }); + it('shouldn\'t matter if derivations are made with strings or numbers', function() { var privateKey = new HDPrivateKey(xprivkey); var derivedByString = privateKey.derive('m/0\'/1/2\''); var derivedByNumber = privateKey.derive(0, true).derive(1).derive(2, true); derivedByNumber.xprivkey.should.equal(derivedByString.xprivkey); }); - }); +