From 98dee7084f63c0a3881e56107eaffefca582a1ba Mon Sep 17 00:00:00 2001 From: Chris Kleeschulte Date: Fri, 30 Jun 2017 08:48:06 -0400 Subject: [PATCH] wip --- lib/services/block/index.js | 158 ++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 53 deletions(-) diff --git a/lib/services/block/index.js b/lib/services/block/index.js index aeb20b99..dc96555e 100644 --- a/lib/services/block/index.js +++ b/lib/services/block/index.js @@ -142,74 +142,101 @@ BlockService.prototype._blockAlreadyProcessed = function(block) { }; +/* + The block service maintains a set of chain tips. This set includes all the block chains that have + been created, including orphaned chains. + + Because blocks can be delievered to us out of order, these out of order blocks enter this collection + as new chains. We won't yet have the block's parent block. + + This creates a unique problem. Until we get a complete chain (including the out of order blocks), we + won't know for sure if a reorg has taken place in the unknown ancestor of the out of order blocks. + So, we have to defer broadcaating blocks until we have a complete chain with unsent blocks. + +*/ BlockService.prototype._mergeBlockIntoChainTips = function(block) { var prevHash = utils.reverseBufferToString(block.header.prevHash); var chain = this._chainTips.get(prevHash); + // No matter what, our own parent can no longer be the tip of any chain this._chainTips.del(prevHash); + // This is the normal case where blocks are received in order. + // We could still be missing blocks from our main chain if there is not a complete + // chain between our tip and this latest block. if (chain) { chain.unshift(prevHash); + chain.set(block.hash, chain); + return; } - var longestChain = this._findLongestChainForHash(prevHash); - var hasChildren = this._setChainOnTip(block.hash, chain, longestChain); + // This is where we have an out of order block arriving, + // but it may fill in gaps in a chain (making that chain the active one). + // We should check for chains that have our hash listed as the last entry. + // This means our children listed us as their parent before now, but now that + // we have arrived, we know our parent's hadh and this may be an tip in another chain. + // So, we put the chains together (think of playing solitaire). + // If unificatiion is done, then this chain becomes the active one. + var chainTips = this._attemptChainUnification(block); - if (!hasChildren) { - this._chainTips.set(block.hash, chain || longestChain); + // if we get more than one chainTip in chainTips, then we have the case where the main chain forked + // whilst we were building orphan chains and waiting for blocks to arrive to fill in the gaps. + // This situation should be a rare event, but can happen. This function won't determine which is the most + // valid chain, but leave it up to others. + + // This is the out of order condition. We can't know which chain we belong to. + // Our hash was not referenced in any chain, therefore our parent wasn't either. + // Only choice to make our hash the tip of its own chain into our parent arrives. + if (chainTips.length < 1) { + this._chainTips.set(block.hash, [prevHash]); } }; -BlockService.prototype._setChainOnTip = function(hash, chain, longestPrevChain) { - - var keys = this._chainTips.keys(); - var hasChildren = false; - - for(var i = 0; i < keys.length; i++) { - - var key = keys[i]; - var searchChain = this._chainTips.get(key); - - - var chainIndex = searchChain.indexOf(hash); - - if (chainIndex > -1) { - hasChildren = true; - this._chainTips.set(key, searchChain.concat(chain || longestPrevChain)); - } - } - - return hasChildren; -}; - -BlockService.prototype._findLongestChainForHash = function(hash) { - - var longestChain = []; - var keys = this._chainTips.keys(); - - for(var i = 0; i < keys.length; i++) { - - var key = keys[i]; - var searchChain = this._chainTips.get(key); - - assert(searchChain.length > 0, 'chain tips collection appears to be invalid'); - - var chainIndex = searchChain.indexOf(hash); - - if (chainIndex > -1) { - - var chain = searchChain.slice(chainIndex); - if (chain.length > longestChain.length) { - longestChain = chain; +BlockService.prototype._findChainTipsWithHash = function(hash, pos) { + // we could have more than one chain that contains this hash as the entry in position (pos) + // (although this would be extremely rare). We should return all the chain tips that apply. + var chainTips = []; + this._chainTips.forEach(function(v, k) { + if (pos === 'last') { + if (v[v.length - 1] === hash) { + return chainTips.push(k); } } + if (v.indexOf(hash) > -1) { + chainTips.push(k); + } + }); + return chainTips; +}; + +/* + The purpose of this function is to look for the opportunity for chains to unify because + new information has arrived (a new block). + + If we have the condition where this block's parent is the tip of a chain -and- our own + block hash is the last block (oldest) in a chain, then unification is possible. + It is possible to find more than one chain where our block hash is the last entry. + In this case, each of those chains forked in blocks that came after us. +*/ +BlockService.prototype._attemptChainUnification = function(block) { + + var prevHash = utils.reverseBufferToString(block.header.prevHash); + + var possibleNewChainTips = this._findChainTipWithHash(block.hash, 'last'); + var orphanChain = this._chainTips[prevHash]; + + if (orphanChain && possibleNewChainTips.length > 0) { + for(var i = 0; i < possibleNewChainTips.length; i++) { + var newChain = this._chainTips[possibleNewChainTips[i]]; + this.chainTips[possibleNewChainTips[i]] = newChain.concat(orphanChain); + } } - longestChain = longestChain.length <= 1 ? [hash] : longestChain; - return longestChain; + return possibleNewChainTips; + }; BlockService.prototype._onBlock = function(block) { @@ -239,19 +266,44 @@ BlockService.prototype._onBlock = function(block) { this.emit('reorg', block); break; default: - var activeChainTip = this._selectActiveChain(block); + // at this point, we know we have a complete chain containing our tip and this new block + var activeChainTip = this._selectActiveChain(); this._sendAllUnsentBlocksFromAcitveChain(activeChainTip); break; } }; -BlockService.prototype._selectActiveChain = function(hash) { - // the active chain is the one that the passed in block hash has as its tip. - // there can only be one without there being a ambiguous situation. - // If more than one chain does have the latest incoming block, then we have a reorg - // situation on our hands and the active chain will be decided elsewhere +/* + Since blocks can arrive out of order from our trusted peer, we can't rely on the latest block + being the tip of the main/active chain. We should, instead, take the chain with the most work completed (the heaviest). + We need not concern ourselves whether or not the block is valid, we trust our peer to do this validation. +*/ +BlockService.prototype._selectActiveChain = function() { + var chainTip; + var mostChainWork = 0; + + this._chainTips.forEach(function(v, k) { + var work = this._computeChainWork(k); + if (work > mostChainWork) { + mostChainWork = work; + chainTip = k; + } + }); + + return chainTip; + +}; + + +BlockService.prototype._computeChainWork = function(chainTip) { + //for super old forks that have cycled out of our cache, just return zero work + var blockHeader = this._blockHeaderQueue.get(chainTip); + if (!blockHeader) { + return 0; + } + blockHeader.chainwork; }; BlockService.prototype._getAllUnsentBlocksFromActiveChain = function(block) {