1 | 'use strict';
|
2 |
|
3 |
|
4 | var assert = require('assert');
|
5 | var expect = require('chai').expect;
|
6 | var should = require('chai').should();
|
7 |
|
8 | var keyLib = require('..');
|
9 | var owsCommon = require('@owstack/ows-common');
|
10 | var Base58Check = owsCommon.encoding.Base58Check;
|
11 | var buffer = require('buffer');
|
12 | var BufferUtil = owsCommon.buffer;
|
13 | var errors = owsCommon.errors;
|
14 | var hdErrors = errors.HDPrivateKey;
|
15 | var HDPrivateKey = keyLib.HDPrivateKey;
|
16 | var Networks = require('@owstack/network-lib');
|
17 | var lodash = require('lodash');
|
18 |
|
19 |
|
20 | require('./data/networks');
|
21 |
|
22 | var xprivkey = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi';
|
23 | var json = '{"network":"btc","depth":0,"fingerPrint":876747070,"parentFingerPrint":0,"childIndex":0,"chainCode":"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508","privateKey":"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35","checksum":-411132559,"xprivkey":"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}';
|
24 |
|
25 | describe('HDPrivate key interface', function() {
|
26 |
|
27 |
|
28 | var expectFail = function(func, error) {
|
29 | var got = null;
|
30 | try {
|
31 | func();
|
32 | } catch (e) {
|
33 | got = e instanceof error;
|
34 | }
|
35 | expect(got).to.equal(true);
|
36 | };
|
37 |
|
38 | var expectDerivationFail = function(argument, error) {
|
39 | return expectFail(function() {
|
40 | var privateKey = new HDPrivateKey(xprivkey);
|
41 | privateKey.deriveChild(argument);
|
42 | }, error);
|
43 | };
|
44 |
|
45 | var expectFailBuilding = function(argument, error) {
|
46 | return expectFail(function() {
|
47 | return new HDPrivateKey(argument);
|
48 | }, error);
|
49 | };
|
50 |
|
51 | var expectSeedFail = function(argument, error) {
|
52 | return expectFail(function() {
|
53 | return HDPrivateKey.fromSeed(argument);
|
54 | }, error);
|
55 | };
|
56 |
|
57 | it('should make a new master private key from random', function() {
|
58 | should.exist(new HDPrivateKey().xprivkey);
|
59 | });
|
60 |
|
61 | it('should make a new private key from random for BTC', function() {
|
62 | var key = new HDPrivateKey('btc');
|
63 | should.exist(key.xprivkey);
|
64 | key.network.name.should.equal('btc');
|
65 | });
|
66 |
|
67 | it('should make a new private key from random for testnet', function() {
|
68 | var key = new HDPrivateKey('testnet');
|
69 | should.exist(key.xprivkey);
|
70 | key.network.name.should.equal('testnet');
|
71 | });
|
72 |
|
73 | it('should not be able to change read-only properties', function() {
|
74 | var key = new HDPrivateKey();
|
75 | key.network.name.should.equal('root');
|
76 | expect(function() {
|
77 | key.fingerPrint = 'notafingerprint';
|
78 | }).to.throw(TypeError);
|
79 | });
|
80 |
|
81 | it('should error with an invalid checksum', function() {
|
82 | expectFailBuilding(xprivkey + '1', errors.InvalidB58Checksum);
|
83 | });
|
84 |
|
85 | it('can be rebuilt from a json generated by itself', function() {
|
86 | var regenerate = new HDPrivateKey(json);
|
87 | regenerate.xprivkey.should.equal(xprivkey);
|
88 | });
|
89 |
|
90 | it('builds a json keeping the structure and same members', function() {
|
91 | assert(lodash.isEqual(
|
92 | new HDPrivateKey(json).toJSON(),
|
93 | new HDPrivateKey(xprivkey).toJSON()
|
94 | ));
|
95 | });
|
96 |
|
97 | describe('instantiation', function() {
|
98 | it('allows no-new calling', function() {
|
99 | HDPrivateKey(xprivkey).toString().should.equal(xprivkey);
|
100 | });
|
101 | it('allows the use of a copy constructor', function() {
|
102 | HDPrivateKey(HDPrivateKey(xprivkey))
|
103 | .xprivkey.should.equal(xprivkey);
|
104 | });
|
105 | });
|
106 |
|
107 | describe('public key', function() {
|
108 | var masterKey = new HDPrivateKey('rprv7v2RjrNn6xnRxmsMPa9aYxq2CW1s4AoAD5bpJXyvebYYsnEHWoMcDrgV9oRj1yxnyfMxiacveX66a3KLfb9gqL1H4tMjLPvBwCTvyxbvcQ3');
|
109 | var ltcKey = new HDPrivateKey('Ltpv71G8qDifUiNetRHkuEut8VA5vEXTBSpXUKZRdUrzq4Z3TuVvDNYh8zuDeyTetUPSqYg5fcEJEDpVqe7tXQaE3YtvJkpYLPjSwPMu3dqfvYh');
|
110 | var btcKey = new HDPrivateKey('xprv9s21ZrQH143K3e39bnn1vyS7YFa1EAJAFGDoeHaSBsgBxgAkTEXeSx7xLvhNQNJxJwhzziWcK3znUFKRPRwWBPkKZ8ijUBa5YYpYPQmeBDX');
|
111 | var testnetKey = new HDPrivateKey('tprv8ZgxMBicQKsPdEeU2KiGFnUgRGriMnQxrwrg6FWCBg4jeiidHRyCCdA357kfkZiGaXEapWZsGDKikeeEbvgXo3UmEdbEKNdQH9VXESmGuUK');
|
112 |
|
113 | it('matches the network', function() {
|
114 | masterKey.publicKey.network.should.equal(Networks.get('root'));
|
115 | ltcKey.publicKey.network.should.equal(Networks.get('ltc'));
|
116 | btcKey.publicKey.network.should.equal(Networks.get('btc'));
|
117 | testnetKey.publicKey.network.should.equal(Networks.get('testnet'));
|
118 | });
|
119 |
|
120 | it('cache for xpubkey works', function() {
|
121 | var privateKey = new HDPrivateKey(xprivkey);
|
122 | should.not.exist(privateKey._hdPublicKey);
|
123 | privateKey.xpubkey.should.equal(privateKey.xpubkey);
|
124 | should.exist(privateKey._hdPublicKey);
|
125 | });
|
126 | });
|
127 |
|
128 | it('inspect() displays correctly', function() {
|
129 | HDPrivateKey(xprivkey).inspect().should.equal('<HDPrivateKey: ' + xprivkey + '>');
|
130 | });
|
131 |
|
132 | it('fails when trying to derive with an invalid argument', function() {
|
133 | expectDerivationFail([], hdErrors.InvalidDerivationArgument);
|
134 | });
|
135 |
|
136 | it('catches early invalid paths', function() {
|
137 | expectDerivationFail('s', hdErrors.InvalidPath);
|
138 | });
|
139 |
|
140 | it('allows derivation of hardened keys by passing a very big number', function() {
|
141 | var privateKey = new HDPrivateKey(xprivkey);
|
142 | var derivedByNumber = privateKey.deriveChild(0x80000000);
|
143 | var derivedByArgument = privateKey.deriveChild(0, true);
|
144 | derivedByNumber.xprivkey.should.equal(derivedByArgument.xprivkey);
|
145 | });
|
146 |
|
147 | it('returns itself with \'m\' parameter', function() {
|
148 | var privateKey = new HDPrivateKey(xprivkey);
|
149 | privateKey.should.equal(privateKey.deriveChild('m'));
|
150 | });
|
151 |
|
152 | it('returns InvalidArgument if invalid data is given to getSerializedError', function() {
|
153 | expect(
|
154 | HDPrivateKey.getSerializedError(1) instanceof hdErrors.UnrecognizedArgument
|
155 | ).to.equal(true);
|
156 | });
|
157 |
|
158 | it('returns InvalidLength if data of invalid length is given to getSerializedError', function() {
|
159 | var b58s = Base58Check.encode(new buffer.Buffer('onestring'));
|
160 | expect(
|
161 | HDPrivateKey.getSerializedError(b58s) instanceof hdErrors.InvalidLength
|
162 | ).to.equal(true);
|
163 | });
|
164 |
|
165 | it('returns InvalidNetworkArgument if an invalid network is provided', function() {
|
166 | expect(
|
167 | HDPrivateKey.getSerializedError(xprivkey, 'invalidNetwork') instanceof errors.InvalidNetworkArgument
|
168 | ).to.equal(true);
|
169 | });
|
170 |
|
171 | it('recognizes that the wrong network was asked for', function() {
|
172 | expect(
|
173 | HDPrivateKey.getSerializedError(xprivkey, 'ltc') instanceof errors.InvalidNetwork
|
174 | ).to.equal(true);
|
175 | });
|
176 |
|
177 | it('recognizes the correct network', function() {
|
178 | expect(HDPrivateKey.getSerializedError(xprivkey, 'btc')).to.equal(null);
|
179 | });
|
180 |
|
181 | describe('on creation from seed', function() {
|
182 | it('converts correctly from an hexa string', function() {
|
183 | should.exist(HDPrivateKey.fromSeed('01234567890abcdef01234567890abcdef').xprivkey);
|
184 | });
|
185 | it('fails when argument is not a buffer or string', function() {
|
186 | expectSeedFail(1, hdErrors.InvalidEntropyArgument);
|
187 | });
|
188 | it('fails when argument doesn\'t provide enough entropy', function() {
|
189 | expectSeedFail('01', hdErrors.InvalidEntropyArgument.NotEnoughEntropy);
|
190 | });
|
191 | it('fails when argument provides too much entropy', function() {
|
192 | var entropy = '0';
|
193 | for (var i = 0; i < 129; i++) {
|
194 | entropy += '1';
|
195 | }
|
196 | expectSeedFail(entropy, hdErrors.InvalidEntropyArgument.TooMuchEntropy);
|
197 | });
|
198 | });
|
199 |
|
200 | it('correctly errors if an invalid checksum is provided', function() {
|
201 | var privKey = new HDPrivateKey(xprivkey);
|
202 | var error = null;
|
203 | try {
|
204 | var buffers = privKey._buffers;
|
205 | buffers.checksum = BufferUtil.integerAsBuffer(0);
|
206 | var privateKey = new HDPrivateKey(buffers);
|
207 | } catch (e) {
|
208 | error = e;
|
209 | }
|
210 | expect(error instanceof errors.InvalidB58Checksum).to.equal(true);
|
211 | });
|
212 |
|
213 | it('correctly validates the checksum', function() {
|
214 | var privKey = new HDPrivateKey(xprivkey);
|
215 | expect(function() {
|
216 | var buffers = privKey._buffers;
|
217 | return new HDPrivateKey(buffers);
|
218 | }).to.not.throw();
|
219 | });
|
220 |
|
221 | it('shouldn\'t matter if derivations are made with strings or numbers', function() {
|
222 | var privateKey = new HDPrivateKey(xprivkey);
|
223 | var derivedByString = privateKey.deriveChild('m/0\'/1/2\'');
|
224 | var derivedByNumber = privateKey.deriveChild(0, true).deriveChild(1).deriveChild(2, true);
|
225 | derivedByNumber.xprivkey.should.equal(derivedByString.xprivkey);
|
226 | });
|
227 |
|
228 | describe('validates paths', function() {
|
229 | it('validates correct paths', function() {
|
230 | var valid;
|
231 |
|
232 | valid = HDPrivateKey.isValidPath('m/0\'/1/2\'');
|
233 | valid.should.equal(true);
|
234 |
|
235 | valid = HDPrivateKey.isValidPath('m');
|
236 | valid.should.equal(true);
|
237 |
|
238 | valid = HDPrivateKey.isValidPath(123, true);
|
239 | valid.should.equal(true);
|
240 |
|
241 | valid = HDPrivateKey.isValidPath(123);
|
242 | valid.should.equal(true);
|
243 |
|
244 | valid = HDPrivateKey.isValidPath(HDPrivateKey.Hardened + 123);
|
245 | valid.should.equal(true);
|
246 |
|
247 | valid = HDPrivateKey.isValidPath(HDPrivateKey.Hardened + 123, true);
|
248 | valid.should.equal(true);
|
249 | });
|
250 |
|
251 | var invalid = [
|
252 | 'm/-1/12',
|
253 | 'bad path',
|
254 | 'K',
|
255 | 'm/',
|
256 | 'm/12asd',
|
257 | 'm/1/2//3'
|
258 | ];
|
259 |
|
260 | invalid.forEach(function(datum) {
|
261 | it('rejects illegal path ' + datum, function() {
|
262 | HDPrivateKey.isValidPath(datum).should.equal(false);
|
263 | expect(HDPrivateKey._getDerivationIndexes(datum)).to.equal(null);
|
264 | });
|
265 | });
|
266 |
|
267 | it('generates deriving indexes correctly', function() {
|
268 | var indexes;
|
269 |
|
270 | indexes = HDPrivateKey._getDerivationIndexes('m/-1/12');
|
271 | expect(indexes).to.equal(null);
|
272 |
|
273 | indexes = HDPrivateKey._getDerivationIndexes('m/0/12/12\'');
|
274 | indexes.should.eql([0, 12, HDPrivateKey.Hardened + 12]);
|
275 |
|
276 | indexes = HDPrivateKey._getDerivationIndexes('m/0/12/12\'');
|
277 | indexes.should.eql([0, 12, HDPrivateKey.Hardened + 12]);
|
278 | });
|
279 |
|
280 | });
|
281 |
|
282 | describe('conversion to/from buffer', function() {
|
283 |
|
284 | var str = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi';
|
285 | it('should roundtrip to/from a buffer', function() {
|
286 | var priv = new HDPrivateKey(str);
|
287 | var toBuffer = priv.toBuffer();
|
288 | var fromBuffer = HDPrivateKey.fromBuffer(toBuffer);
|
289 | var roundTrip = new HDPrivateKey(fromBuffer.toBuffer());
|
290 | roundTrip.xprivkey.should.equal(str);
|
291 | });
|
292 |
|
293 | });
|
294 |
|
295 | describe('conversion to plain object/json', function() {
|
296 |
|
297 | var plainObject = {
|
298 | 'network': 'btc',
|
299 | 'depth': 0,
|
300 | 'fingerPrint': 876747070,
|
301 | 'parentFingerPrint': 0,
|
302 | 'childIndex': 0,
|
303 | 'chainCode': '873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508',
|
304 | 'privateKey': 'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35',
|
305 | 'checksum': -411132559,
|
306 | 'xprivkey': 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvN' +
|
307 | 'KmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'
|
308 | };
|
309 |
|
310 | it('toObject leaves no Buffer instances', function() {
|
311 | var privKey = new HDPrivateKey(xprivkey);
|
312 | var object = privKey.toObject();
|
313 | lodash.each(lodash.values(object), function(value) {
|
314 | expect(BufferUtil.isBuffer(value)).to.equal(false);
|
315 | });
|
316 | });
|
317 |
|
318 | it('roundtrips toObject', function() {
|
319 | expect(HDPrivateKey.fromObject(new HDPrivateKey(xprivkey).toObject()).xprivkey).to.equal(xprivkey);
|
320 | });
|
321 |
|
322 | it('roundtrips to JSON and to Object', function() {
|
323 | var privkey = new HDPrivateKey(xprivkey);
|
324 | expect(HDPrivateKey.fromObject(privkey.toJSON()).xprivkey).to.equal(xprivkey);
|
325 | });
|
326 |
|
327 | it('recovers state from JSON', function() {
|
328 | new HDPrivateKey(JSON.stringify(plainObject)).xprivkey.should.equal(xprivkey);
|
329 | });
|
330 |
|
331 | it('recovers state from Object', function() {
|
332 | new HDPrivateKey(plainObject).xprivkey.should.equal(xprivkey);
|
333 | });
|
334 |
|
335 | });
|
336 | });
|