From c0fd199f2d55b625336bb3ca2ff1beb77cd74d5c Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 25 Nov 2016 18:54:02 -0800 Subject: [PATCH] chain: refactor and optimize. --- lib/blockchain/chain.js | 34 +++---- lib/blockchain/chaindb.js | 180 +++++++++++++++++++++-------------- lib/blockchain/chainentry.js | 65 ++++++------- lib/http/rpc.js | 12 +-- lib/http/server.js | 2 +- lib/net/peer.js | 2 +- lib/node/nodeclient.js | 2 +- test/chain-test.js | 12 +-- 8 files changed, 168 insertions(+), 141 deletions(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index c6d5144c..8bd5c406 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -1084,7 +1084,7 @@ Chain.prototype.replay = co(function* replay(block) { */ Chain.prototype._replay = co(function* replay(block) { - var entry = yield this.db.get(block); + var entry = yield this.db.getEntry(block); if (!entry) throw new Error('Block not found.'); @@ -1255,7 +1255,7 @@ Chain.prototype._add = co(function* add(block) { throw new VerifyError(block, 'invalid', ret.reason, ret.score); } - existing = yield this.db.has(hash); + existing = yield this.db.hasEntry(hash); // Do we already have this block? if (existing) { @@ -1264,7 +1264,7 @@ Chain.prototype._add = co(function* add(block) { } // Find the previous block height/index. - prev = yield this.db.get(prevBlock); + prev = yield this.db.getEntry(prevBlock); if (prev) height = prev.height + 1; @@ -1547,7 +1547,7 @@ Chain.prototype.has = co(function* has(hash) { if (hash === this.currentBlock) return true; - return yield this.hasBlock(hash); + return yield this.hasEntry(hash); }); /** @@ -1569,7 +1569,7 @@ Chain.prototype.byTime = co(function* byTime(ts) { // timestamp. while (start < end) { pos = (start + end) >>> 1; - entry = yield this.db.get(pos); + entry = yield this.db.getEntry(pos); if (!entry) return; @@ -1594,8 +1594,8 @@ Chain.prototype.byTime = co(function* byTime(ts) { * @returns {Promise} - Returns Boolean. */ -Chain.prototype.hasBlock = function hasBlock(hash) { - return this.db.has(hash); +Chain.prototype.hasEntry = function hasEntry(hash) { + return this.db.hasEntry(hash); }; /** @@ -1625,7 +1625,7 @@ Chain.prototype.hasPending = function hasPending(hash) { */ Chain.prototype.getEntry = function getEntry(hash, callback) { - return this.db.get(hash); + return this.db.getEntry(hash); }; /** @@ -1715,7 +1715,7 @@ Chain.prototype._getLocator = co(function* getLocator(start) { if (start == null) start = this.tip.hash; - entry = yield this.db.get(start); + entry = yield this.db.getEntry(start); if (!entry) { // We could simply return `start` here, @@ -1755,7 +1755,7 @@ Chain.prototype._getLocator = co(function* getLocator(start) { continue; } - entry = yield entry.getAncestorByHeight(height); + entry = yield entry.getAncestor(height); if (!entry) break; @@ -1956,7 +1956,7 @@ Chain.prototype.getState = co(function* getState(prev, deployment) { var thresholdStates = constants.thresholdStates; var bit = deployment.bit; var compute = []; - var i, entry, count, state; + var i, entry, count, state, cached; var block, time, height; if (!prev) @@ -1964,7 +1964,7 @@ Chain.prototype.getState = co(function* getState(prev, deployment) { if (((prev.height + 1) % period) !== 0) { height = prev.height - ((prev.height + 1) % period); - prev = yield prev.getAncestorByHeight(height); + prev = yield prev.getAncestor(height); if (prev) { assert(prev.height === height); @@ -1976,8 +1976,10 @@ Chain.prototype.getState = co(function* getState(prev, deployment) { state = thresholdStates.DEFINED; while (entry) { - if (this.db.stateCache.get(bit, entry) !== -1) { - state = this.db.stateCache.get(bit, entry); + cached = this.db.stateCache.get(bit, entry); + + if (cached !== -1) { + state = cached; break; } @@ -1992,7 +1994,7 @@ Chain.prototype.getState = co(function* getState(prev, deployment) { compute.push(entry); height = entry.height - period; - entry = yield entry.getAncestorByHeight(height); + entry = yield entry.getAncestor(height); } while (compute.length) { @@ -2161,7 +2163,7 @@ Chain.prototype.getLocks = co(function* getLocks(prev, tx, flags) { continue; } - entry = yield prev.getAncestorByHeight(Math.max(coinHeight - 1, 0)); + entry = yield prev.getAncestor(Math.max(coinHeight - 1, 0)); assert(entry, 'Database is corrupt.'); coinTime = yield entry.getMedianTimeAsync(); diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index 838ea0d2..4385f17b 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -83,7 +83,7 @@ function ChainDB(chain) { this.cacheWindow = (this.network.pow.retargetInterval + 1) * 2 + 100; // We want to keep the last 5 blocks of unspents in memory. - this.coinWindow = 25 << 20; + this.coinWindow = 65 << 20; this.coinCache = new LRU.Nil(); this.cacheHash = new LRU(this.cacheWindow); @@ -280,32 +280,32 @@ ChainDB.prototype.commit = co(function* commit() { /** * Test the cache for a present entry hash or height. - * @param {Hash|Number} hash - Hash or height. + * @param {Hash|Number} block - Hash or height. */ -ChainDB.prototype.hasCache = function hasCache(hash) { - checkHash(hash); +ChainDB.prototype.hasCache = function hasCache(block) { + if (typeof block === 'number') + return this.cacheHeight.has(block); - if (typeof hash === 'number') - return this.cacheHeight.has(hash); + assert(typeof block === 'string'); - return this.cacheHash.has(hash); + return this.cacheHash.has(block); }; /** * Get an entry directly from the LRU cache. This is * useful for optimization if we don't want to wait on a * nextTick during a `get()` call. - * @param {Hash|Number} hash - Hash or height. + * @param {Hash|Number} block - Hash or height. */ -ChainDB.prototype.getCache = function getCache(hash) { - checkHash(hash); +ChainDB.prototype.getCache = function getCache(block) { + if (typeof block === 'number') + return this.cacheHeight.get(block); - if (typeof hash === 'number') - return this.cacheHeight.get(hash); + assert(typeof block === 'string'); - return this.cacheHash.get(hash); + return this.cacheHash.get(block); }; /** @@ -317,11 +317,11 @@ ChainDB.prototype.getCache = function getCache(hash) { ChainDB.prototype.getHeight = co(function* getHeight(hash) { var entry, height; - checkHash(hash); - if (typeof hash === 'number') return hash; + assert(typeof hash === 'string'); + if (hash === constants.NULL_HASH) return -1; @@ -348,11 +348,14 @@ ChainDB.prototype.getHeight = co(function* getHeight(hash) { ChainDB.prototype.getHash = co(function* getHash(height) { var entry, hash; - checkHash(height); - if (typeof height === 'string') return height; + assert(typeof height === 'number'); + + if (height < 0) + return; + entry = this.cacheHeight.get(height); if (entry) @@ -366,37 +369,17 @@ ChainDB.prototype.getHash = co(function* getHash(height) { return hash.toString('hex'); }); -/** - * Get the current chain height from the tip record. - * @returns {Promise} - Returns Number. - */ - -ChainDB.prototype.getChainHeight = co(function* getChainHeight() { - var entry = yield this.getTip(); - - if (!entry) - return -1; - - return entry.height; -}); - /** * Get both hash and height depending on the value passed in. - * @param {Hash|Number} block - Can be a has or height. - * @returns {Promise} - Returns {@link Hash}, Number. + * @param {Hash|Number} block - Can be a hash or height. + * @returns {Promise} */ ChainDB.prototype.getBoth = co(function* getBoth(block) { var hash, height; - checkHash(block); - - if (typeof block === 'string') - hash = block; - else + if (typeof block === 'number') { height = block; - - if (!hash) { hash = yield this.getHash(height); if (hash == null) @@ -405,6 +388,9 @@ ChainDB.prototype.getBoth = co(function* getBoth(block) { return new BlockPair(hash, height); } + assert(typeof block === 'string'); + + hash = block; height = yield this.getHeight(hash); if (height === -1) @@ -414,19 +400,58 @@ ChainDB.prototype.getBoth = co(function* getBoth(block) { }); /** - * Retrieve a chain entry but do _not_ add it to the LRU cache. + * Retrieve a chain entry by height. + * @param {Number} height + * @returns {Promise} - Returns {@link ChainEntry}. + */ + +ChainDB.prototype.getEntryByHeight = co(function* getEntryByHeight(height) { + var state, entry, hash; + + assert(typeof height === 'number'); + + if (height < 0) + return; + + entry = this.cacheHeight.get(height); + + if (entry) + return entry; + + hash = yield this.db.get(layout.H(height)); + + if (!hash) + return; + + hash = hash.toString('hex'); + state = this.chain.state; + + entry = yield this.getEntryByHash(hash); + + if (!entry) + return; + + // By the time getEntry has completed, + // a reorg may have occurred. This entry + // may not be on the main chain anymore. + if (this.chain.state === state) + this.cacheHeight.set(entry.height, entry); + + return entry; +}); + +/** + * Retrieve a chain entry by hash. * @param {Hash} hash * @returns {Promise} - Returns {@link ChainEntry}. */ -ChainDB.prototype.getEntry = co(function* getEntry(hash) { - var entry; +ChainDB.prototype.getEntryByHash = co(function* getEntryByHash(hash) { + var entry, raw; - checkHash(hash); + assert(typeof hash === 'string'); - hash = yield this.getHash(hash); - - if (!hash) + if (hash === constants.NULL_HASH) return; entry = this.cacheHash.get(hash); @@ -434,25 +459,12 @@ ChainDB.prototype.getEntry = co(function* getEntry(hash) { if (entry) return entry; - entry = yield this.db.get(layout.e(hash)); + raw = yield this.db.get(layout.e(hash)); - if (!entry) + if (!raw) return; - return ChainEntry.fromRaw(this.chain, entry); -}); - -/** - * Retrieve a chain entry and add it to the LRU cache. - * @param {Hash} hash - * @returns {Promise} - Returns {@link ChainEntry}. - */ - -ChainDB.prototype.get = co(function* get(hash) { - var entry = yield this.getEntry(hash); - - if (!entry) - return; + entry = ChainEntry.fromRaw(this.chain, raw); // There's no efficient way to check whether // this is in the main chain or not, so @@ -462,6 +474,18 @@ ChainDB.prototype.get = co(function* get(hash) { return entry; }); +/** + * Retrieve a chain entry. + * @param {Number|Hash} block - Height or hash. + * @returns {Promise} - Returns {@link ChainEntry}. + */ + +ChainDB.prototype.getEntry = function getEntry(block) { + if (typeof block === 'number') + return this.getEntryByHeight(block); + return this.getEntryByHash(block); +}; + /** * Test whether the chain contains a block in the * main chain or an alternate chain. Alternate chains will only @@ -470,7 +494,7 @@ ChainDB.prototype.get = co(function* get(hash) { * @returns {Promise} - Returns Boolean. */ -ChainDB.prototype.has = co(function* has(block) { +ChainDB.prototype.hasEntry = co(function* hasEntry(block) { var item = yield this.getBoth(block); return item.hash != null; }); @@ -481,7 +505,7 @@ ChainDB.prototype.has = co(function* has(block) { */ ChainDB.prototype.getTip = function getTip() { - return this.get(this.state.hash); + return this.getEntry(this.state.hash); }; /** @@ -534,6 +558,8 @@ ChainDB.prototype.getNextHash = co(function* getNextHash(hash) { */ ChainDB.prototype.isMainChain = co(function* isMainChain(hash) { + var entry; + assert(typeof hash === 'string'); if (hash === this.chain.tip.hash @@ -541,6 +567,17 @@ ChainDB.prototype.isMainChain = co(function* isMainChain(hash) { return true; } + if (hash === constants.NULL_HASH) + return false; + + entry = this.cacheHash.get(hash); + + if (entry) { + entry = this.cacheHeight.get(entry.height); + if (entry) + return entry.hash === hash; + } + if (yield this.getNextHash(hash)) return true; @@ -1373,7 +1410,7 @@ ChainDB.prototype.saveUpdates = function saveUpdates() { */ ChainDB.prototype.reset = co(function* reset(block) { - var entry = yield this.get(block); + var entry = yield this.getEntry(block); var tip; if (!entry) @@ -1436,7 +1473,7 @@ ChainDB.prototype.reset = co(function* reset(block) { this.cacheHeight.remove(tip.height); this.cacheHash.remove(tip.hash); - tip = yield this.get(tip.prevBlock); + tip = yield this.getEntry(tip.prevBlock); assert(tip); } @@ -1475,7 +1512,7 @@ ChainDB.prototype.removeChains = co(function* removeChains() { */ ChainDB.prototype._removeChain = co(function* removeChain(hash) { - var tip = yield this.get(hash); + var tip = yield this.getEntry(hash); if (!tip) throw new Error('Alternate chain tip not found.'); @@ -1498,7 +1535,7 @@ ChainDB.prototype._removeChain = co(function* removeChain(hash) { // on successful write. this.cacheHash.unpush(tip.hash); - tip = yield this.get(tip.prevBlock); + tip = yield this.getEntry(tip.prevBlock); assert(tip); } }); @@ -2140,11 +2177,6 @@ function getSize(value) { return 80 + value.length; } -function checkHash(hash) { - assert(typeof hash === 'string' || typeof hash === 'number', - 'Must pass in height or hash.'); -} - function BlockPair(hash, height) { this.hash = hash; this.height = height; diff --git a/lib/blockchain/chainentry.js b/lib/blockchain/chainentry.js index 766ab457..737e8c2c 100644 --- a/lib/blockchain/chainentry.js +++ b/lib/blockchain/chainentry.js @@ -191,8 +191,6 @@ ChainEntry.prototype.getAncestors = co(function* getAncestors(max) { assert(util.isNumber(max)); - // Try to do this iteratively and synchronously - // so we don't have to wait on nextTicks. for (;;) { ancestors.push(entry); @@ -247,61 +245,38 @@ ChainEntry.prototype.isMainChain = co(function* isMainChain() { }); /** - * Collect ancestors up to `height`. + * Get ancestor by `height`. * @param {Number} height * @returns {Promise} - Returns ChainEntry[]. */ -ChainEntry.prototype.getAncestorByHeight = co(function* getAncestorByHeight(height) { - var entry; +ChainEntry.prototype.getAncestor = co(function* getAncestor(height) { + var entry = this; if (height < 0) - return yield co.wait(); + return; assert(height >= 0); assert(height <= this.height); if (yield this.isMainChain()) - return yield this.chain.db.get(height); + return yield this.chain.db.getEntry(height); - entry = yield this.getAncestor(this.height - height); - - if (!entry) - return; - - assert(entry.height === height); + while (entry.height !== height) { + entry = yield entry.getPrevious(); + assert(entry); + } return entry; }); -/** - * Get a single ancestor by index. Note that index-0 is - * the same entry. This is done for sane porting of - * bitcoind functions to BCoin. - * @param {Number} index - * @returns {Function} callback - Returns [Error, ChainEntry]. - */ - -ChainEntry.prototype.getAncestor = co(function* getAncestor(index) { - var ancestors; - - assert(index >= 0); - - ancestors = yield this.getAncestors(index + 1); - - if (ancestors.length < index + 1) - return; - - return ancestors[index]; -}); - /** * Get previous entry. * @returns {Promise} - Returns ChainEntry. */ ChainEntry.prototype.getPrevious = function getPrevious() { - return this.chain.db.get(this.prevBlock); + return this.chain.db.getEntry(this.prevBlock); }; /** @@ -313,7 +288,25 @@ ChainEntry.prototype.getNext = co(function* getNext() { var hash = yield this.chain.db.getNextHash(this.hash); if (!hash) return; - return yield this.chain.db.get(hash); + return yield this.chain.db.getEntry(hash); +}); + +/** + * Get next entry. + * @returns {Promise} - Returns ChainEntry. + */ + +ChainEntry.prototype.getNextEntry = co(function* getNextEntry() { + var entry = yield this.chain.db.getEntry(this.height + 1); + + if (!entry) + return; + + // Not on main chain. + if (entry.prevBlock !== this.hash) + return; + + return entry; }); /** diff --git a/lib/http/rpc.js b/lib/http/rpc.js index ffc79c87..7e8be6e8 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -738,7 +738,7 @@ RPC.prototype.getblock = co(function* getblock(args) { if (args.length > 1) verbose = toBool(args[1]); - entry = yield this.chain.db.get(hash); + entry = yield this.chain.db.getEntry(hash); if (!entry) throw new RPCError('Block not found'); @@ -836,7 +836,7 @@ RPC.prototype.getblockhash = co(function* getblockhash(args) { if (height < 0 || height > this.chain.height) throw new RPCError('Block height out of range.'); - entry = yield this.chain.db.get(height); + entry = yield this.chain.db.getEntry(height); if (!entry) throw new RPCError('Not found.'); @@ -860,7 +860,7 @@ RPC.prototype.getblockheader = co(function* getblockheader(args) { if (args.length > 1) verbose = toBool(args[1], true); - entry = yield this.chain.db.get(hash); + entry = yield this.chain.db.getEntry(hash); if (!entry) throw new RPCError('Block not found'); @@ -935,7 +935,7 @@ RPC.prototype.getchaintips = co(function* getchaintips(args) { for (i = 0; i < tips.length; i++) { hash = tips[i]; - entry = yield this.chain.db.get(hash); + entry = yield this.chain.db.getEntry(hash); assert(entry); fork = yield this._findFork(entry); @@ -1253,7 +1253,7 @@ RPC.prototype.verifytxoutproof = co(function* verifytxoutproof(args) { if (!block.verify()) return res; - entry = yield this.chain.db.get(block.hash('hex')); + entry = yield this.chain.db.getEntry(block.hash('hex')); if (!entry) throw new RPCError('Block not found in chain.'); @@ -1848,7 +1848,7 @@ RPC.prototype._hashps = co(function* _hashps(lookup, height) { var i, minTime, maxTime, workDiff, timeDiff, ps, entry; if (height !== -1) - tip = yield this.chain.db.get(height); + tip = yield this.chain.db.getEntry(height); if (!tip) return 0; diff --git a/lib/http/server.js b/lib/http/server.js index a22ad7b8..f7654d4f 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -1389,7 +1389,7 @@ HTTPServer.prototype._initIO = function _initIO() { } try { - entry = yield self.chain.db.get(block); + entry = yield self.chain.db.getEntry(block); if (!(yield entry.isMainChain())) entry = null; } catch (e) { diff --git a/lib/net/peer.js b/lib/net/peer.js index 8eb14e55..2bcfe4b7 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -1405,7 +1405,7 @@ Peer.prototype._handleGetHeaders = co(function* _handleGetHeaders(packet) { } if (hash) - entry = yield this.chain.db.get(hash); + entry = yield this.chain.db.getEntry(hash); while (entry) { headers.push(entry.toHeaders()); diff --git a/lib/node/nodeclient.js b/lib/node/nodeclient.js index 7ad40a19..4a4e0598 100644 --- a/lib/node/nodeclient.js +++ b/lib/node/nodeclient.js @@ -106,7 +106,7 @@ NodeClient.prototype.getTip = function getTip() { */ NodeClient.prototype.getEntry = co(function* getEntry(hash) { - var entry = yield this.node.chain.db.get(hash); + var entry = yield this.node.chain.db.getEntry(hash); if (!entry) return; diff --git a/test/chain-test.js b/test/chain-test.js index 5ed1b7c9..9e03d147 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -109,8 +109,8 @@ describe('Chain', function() { assert(chain.tip.hash === block1.hash('hex')); - tip1 = yield chain.db.get(block1.hash('hex')); - tip2 = yield chain.db.get(block2.hash('hex')); + tip1 = yield chain.db.getEntry(block1.hash('hex')); + tip2 = yield chain.db.getEntry(block2.hash('hex')); assert(tip1); assert(tip2); @@ -135,7 +135,7 @@ describe('Chain', function() { assert.equal(walletdb.state.height, chain.height); assert.equal(chain.height, 11); - entry = yield chain.db.get(tip2.hash); + entry = yield chain.db.getEntry(tip2.hash); assert(entry); assert(chain.height === entry.height); @@ -178,7 +178,7 @@ describe('Chain', function() { deleteCoins(block); yield chain.add(block); - entry = yield chain.db.get(block.hash('hex')); + entry = yield chain.db.getEntry(block.hash('hex')); assert(entry); assert(chain.tip.hash === entry.hash); @@ -228,8 +228,8 @@ describe('Chain', function() { assert.equal(balance.unconfirmed, 1250 * 1e8); assert.equal(balance.confirmed, 750 * 1e8); - assert(wallet.account.receiveDepth >= 8); - assert(wallet.account.changeDepth >= 7); + assert(wallet.account.receiveDepth >= 7); + assert(wallet.account.changeDepth >= 6); assert.equal(walletdb.state.height, chain.height);