diff --git a/index.js b/index.js index 7139d37..858d6a0 100644 --- a/index.js +++ b/index.js @@ -18,11 +18,13 @@ bitcore.encoding.BufferReader = require('./lib/encoding/bufferreader'); bitcore.encoding.BufferWriter = require('./lib/encoding/bufferwriter'); bitcore.encoding.Varint = require('./lib/encoding/varint'); +// utilities bitcore.util = {}; bitcore.util.bitcoin = require('./lib/util/bitcoin'); bitcore.util.buffer = require('./lib/util/buffer'); bitcore.util.js = require('./lib/util/js'); +// errors thrown by the library bitcore.errors = require('./lib/errors'); // main bitcoin library @@ -41,7 +43,7 @@ bitcore.Txin = require('./lib/txin'); bitcore.Txout = require('./lib/txout'); -//dependencies, subject to change +// dependencies, subject to change bitcore.deps = {}; bitcore.deps.bnjs = require('bn.js'); bitcore.deps.bs58 = require('bs58'); @@ -53,3 +55,6 @@ bitcore.deps.elliptic = require('elliptic'); //bitcore.txpartial = require('lib/txpartial'); //bitcore.bip70 = require('lib/bip70'); + +// Internal usage, exposed for testing/advanced tweaking +bitcore._HDKeyCache = require('./lib/hdkeycache'); diff --git a/lib/hdkeycache.js b/lib/hdkeycache.js index d2e566b..721d9da 100644 --- a/lib/hdkeycache.js +++ b/lib/hdkeycache.js @@ -1,16 +1,45 @@ 'use strict'; -var cache = {}; - module.exports = { + _cache: {}, + _count: 0, + _eraseIndex: 0, + _usedList: {}, + _usedIndex: {}, + _CACHE_SIZE: 5000, + get: function(xkey, number, hardened) { + hardened = !!hardened; var key = xkey + '/' + number + '/' + hardened; - if (cache[key]) { - return cache[key]; + if (this._cache[key]) { + this._cacheHit(key); + return this._cache[key]; } }, set: function(xkey, number, hardened, derived) { + hardened = !!hardened; var key = xkey + '/' + number + '/' + hardened; - cache[key] = derived; + this._cache[key] = derived; + this._cacheHit(key); + }, + _cacheHit: function(key) { + if (this._usedIndex[key]) { + delete this._usedList[this._usedIndex[key]]; + } + this._usedList[this._count] = key; + this._usedIndex[key] = this._count; + this._count++; + this._cacheRemove(); + }, + _cacheRemove: function() { + while (this._eraseIndex < this._count - this._CACHE_SIZE) { + if (this._usedList[this._eraseIndex]) { + var removeKey = this._usedList[this._eraseIndex]; + delete this._usedIndex[removeKey]; + delete this._cache[removeKey]; + } + delete this._usedList[this._eraseIndex]; + this._eraseIndex++; + } } }; diff --git a/test/hdkeycache.js b/test/hdkeycache.js new file mode 100644 index 0000000..57635d4 --- /dev/null +++ b/test/hdkeycache.js @@ -0,0 +1,50 @@ +'use strict'; + +var _ = require('lodash'); +var expect = require('chai').expect; +var bitcore = require('..'); +var HDPrivateKey = bitcore.HDPrivateKey; + +var xprivkey = 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; + +describe('HDKey cache', function() { + /* jshint unused: false */ + var cache = bitcore._HDKeyCache; + var master = new HDPrivateKey(xprivkey); + + beforeEach(function() { + cache._cache = {}; + cache._count = 0; + cache._eraseIndex = 0; + cache._usedIndex = {}; + cache._usedList = {}; + cache._CACHE_SIZE = 3; // Reduce for quick testing + }); + + it('saves a derived key', function() { + var child = master.derive(0); + expect(cache._cache[master.xprivkey + '/0/false'].xprivkey).to.equal(child.xprivkey); + }); + it('starts erasing unused keys', function() { + var child1 = master.derive(0); + var child2 = child1.derive(0); + var child3 = child2.derive(0); + expect(cache._cache[master.xprivkey + '/0/false'].xprivkey).to.equal(child1.xprivkey); + var child4 = child3.derive(0); + expect(cache._cache[master.xprivkey + '/0/false']).to.equal(undefined); + }); + it('avoids erasing keys that get cache hits ("hot keys")', function() { + var child1 = master.derive(0); + var child2 = master.derive(0).derive(0); + expect(cache._cache[master.xprivkey + '/0/false'].xprivkey).to.equal(child1.xprivkey); + var child1_copy = master.derive(0); + expect(cache._cache[master.xprivkey + '/0/false'].xprivkey).to.equal(child1.xprivkey); + }); + it('keeps the size of the cache small', function() { + var child1 = master.derive(0); + var child2 = child1.derive(0); + var child3 = child2.derive(0); + var child4 = child3.derive(0); + expect(_.size(cache._cache)).to.equal(3); + }); +});