diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index ce7f2208..806b0307 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -58,7 +58,7 @@ function Chain(options) { { hash: utils.toHex(network.genesis._hash), version: network.genesis.version, - // prevBlock: utils.toHex(network.genesis.prevBlock), + prevBlock: utils.toHex(network.genesis.prevBlock), ts: network.genesis.ts, bits: network.genesis.bits, height: 0 @@ -158,6 +158,11 @@ Chain.prototype._addIndex = function _addIndex(entry) { if (checkpoint) { this.emit('checkpoint', entry.height, entry.hash, checkpoint); if (hash !== checkpoint) { + // Resetting to the last checkpoint _really_ isn't + // necessary (even bitcoind doesn't do it), but it + // could be used if you want to be on the overly + // safe (see: paranoid) side. + // this.resetLastCheckpoint(entry.height); this.emit('fork', entry.height, entry.hash, checkpoint); return Chain.codes.badCheckpoint; } @@ -177,12 +182,18 @@ Chain.prototype._addIndex = function _addIndex(entry) { }; Chain.prototype.resetLastCheckpoint = function resetLastCheckpoint(height) { - var lastHeight = Object.keys(network.checkpoints).sort().indexOf(height) - 1; + var heights = Object.keys(network.checkpoints).sort(); + var index = heights.indexOf(height) - 1; + var checkpoint = network.checkpoint[index]; - if (lastHeight < 0) - lastHeight = 0; + assert(index >= 0); + assert(checkpoint); - this.resetHeight(lastHeight); + // This is the safest way to do it, the other possibility is to simply reset + // ignore the bad checkpoint block. The likelihood of someone carrying on an + // entire fork between to checkpoints is absurd, so this is probably _a lot_ + // of work for nothing. + this.resetHeight(checkpoint.height); }; Chain.prototype.resetHeight = function resetHeight(height) { @@ -191,14 +202,21 @@ Chain.prototype.resetHeight = function resetHeight(height) { assert(height < this.index.entries.length); + // Nothing to do. if (height === this.index.entries.length - 1) return; + // Reset the orphan map completely. There may + // have been some orphans on a forked chain we + // no longer need. this.orphan.map = {}; this.orphan.bmap = {}; this.orphan.count = 0; this.orphan.size = 0; + + // Rebuild the index from our new (lower) height. this.index.entries.length = height + 1; + this.index.heights = this.index.entries.reduce(function(out, entry) { if (!self.options.fullNode) { if (!entry) @@ -207,6 +225,7 @@ Chain.prototype.resetHeight = function resetHeight(height) { out[entry.hash] = entry.height; return out; }, {}); + this.index.hashes.length = height + 1; if (!this.options.fullNode) @@ -214,14 +233,20 @@ Chain.prototype.resetHeight = function resetHeight(height) { else this.index.count = height + 1; + // Set and emit our new (old) tip. this.tip = this.index.entries[this.index.entries.length - 1]; this.emit('tip', this.tip); + // The lastTs is supposed to be the last ts + // after the preload, but we're not sure where + // we're resetting to. It may be lower, it may + // be higher. Reset it if necessary. this.index.lastTs = Math.min( this.index.lastTs, this.index.entries[this.index.entries.length - 1].ts ); + // Delete all the blocks now above us. ahead.forEach(function(entry) { if (!self.options.fullNode) { if (!entry) @@ -239,6 +264,10 @@ Chain.prototype.resetTime = function resetTime(ts) { }; Chain.prototype.add = function add(block, peer) { + var initial = block; + var code = Chain.codes.unchanged; + var hash, prevHash, prevHeight, entry; + if (this.loading) { this.once('load', function() { this.add(block); @@ -246,33 +275,35 @@ Chain.prototype.add = function add(block, peer) { return; } - var initial = block; - var code = Chain.codes.unchanged; - var hash, prev, i, entry; - for (;;) { - // Only validate the initial block (orphans were already validated) + // Validate the block we want to add. + // This is only necessary for new + // blocks coming in, not the resolving + // orphans. if (block === initial && !block.verify()) { code = Chain.codes.invalid; break; } hash = block.hash('hex'); - prev = block.prevBlock; + prevHash = block.prevBlock; - // If the block is already known to be an orphan - if (this.orphan.map[prev]) { + // If the block is already known to be + // an orphan, ignore it. + if (this.orphan.map[prevHash]) { code = Chain.codes.knownOrphan; break; } - i = this.index.heights[prev]; + // Find the previous block height/index. + prevHeight = this.index.heights[prevHash]; - // If previous block wasn't ever seen - add current to orphans - if (i == null) { + // If previous block wasn't ever seen, + // add it current to orphans and break. + if (prevHeight == null) { this.orphan.count++; this.orphan.size += block.size(); - this.orphan.map[prev] = block; + this.orphan.map[prevHash] = block; this.orphan.bmap[hash] = block; code = Chain.codes.newOrphan; break; @@ -281,50 +312,67 @@ Chain.prototype.add = function add(block, peer) { entry = new ChainBlock(this, { hash: hash, version: block.version, - // prevBlock: prev, + prevBlock: prevHash, ts: block.ts, bits: block.bits, - height: i + 1 + height: prevHeight + 1 }); - // Add entry if we do not have it (or there is another hash at its height) + // Add entry if we do not have it (or if + // there is another entry at its height) if (this.index.hashes[entry.height] !== hash) { assert(this.index.heights[entry.hash] == null); - // If we have a block at the same height, use chain with higher work + // A valid block with an already existing + // height came in, that spells fork. We + // don't store by hash so we can't compare + // chainworks. We reset the chain, find a + // new peer, and wait to see who wins. if (this.index.hashes[entry.height]) { - if (this.tip.chainwork.cmp(entry.chainwork) < 0) { - if (!block.postVerify()) { - throw new Error; - //code = Chain.codes.invalid; - //break; - } - this.resetHeight(entry.height - 1); - code = this._addIndex(entry); - assert(code !== Chain.codes.unchanged); - if (code !== Chain.codes.okay) - break; - code = Chain.codes.forked; - // Breaking here only works because - // we deleted the orphan map in resetHeight. - this.emit('block', block, peer); - this.emit('entry', entry); - if (block !== initial) - this.emit('resolved', entry); + // The tip has more chainwork, it is a + // higher height than the entry. This is + // not an alternate tip. Ignore it. + if (this.tip.chainwork.cmp(entry.chainwork) > 0) { + code = Chain.codes.unchanged; break; } + // The block has equal chainwork (an + // alternate tip). Reset the chain, find + // a new peer, and wait to see who wins. + this.resetHeight(entry.height - 1); + this.emit('fork', entry.height, entry.hash); + code = Chain.codes.forked; + break; } - // Validated known block at this point - add it to index + // Do "contextual" verification on our block + // now that we're certain it's previous + // block is in the chain. if (!block.postVerify()) { throw new Error; //code = Chain.codes.invalid; //break; } + + // Attempt to add block to the chain index. code = this._addIndex(entry); + + // Result should never be `unchanged` since + // we already verified there were no + // duplicate heights, etc. assert(code !== Chain.codes.unchanged); + + // Block did not match the checkpoint. The + // chain could be reset to the last sane + // checkpoint, but it really isn't necessary, + // so we don't do it. The misbehaving peer has + // been killed and hopefully we find a peer + // who isn't trying to fool us. if (code !== Chain.codes.okay) break; + + // Emit our block (and potentially resolved + // orphan) so the programmer can save it. this.emit('block', block, peer); this.emit('entry', entry); if (block !== initial) @@ -337,7 +385,7 @@ Chain.prototype.add = function add(block, peer) { if (!this.orphan.map[hash]) break; - // We have orphan child for this block - add it to chain + // An orphan chain was found, start resolving. block = this.orphan.map[hash]; delete this.orphan.bmap[block.hash('hex')]; delete this.orphan.map[hash]; @@ -345,7 +393,8 @@ Chain.prototype.add = function add(block, peer) { this.orphan.size -= block.size(); } - // Failsafe for large orphan chains + // Failsafe for large orphan chains. Do not + // allow more than 20mb stored in memory. if (this.orphan.size > 20971520) { this.orphan.map = {}; this.orphan.bmap = {}; @@ -353,7 +402,8 @@ Chain.prototype.add = function add(block, peer) { this.orphan.size = 0; } - // No need to have a huge chain + // Potentially compact the chain here. A + // full chain is not necessary for spv. // if (!this.options.fullNode) { // if (this.size() > 100000) // this.compact(); @@ -477,7 +527,11 @@ Chain.prototype.locatorHashes = function locatorHashes(start) { if (typeof start === 'string') { top = this.index.heights[start]; if (top == null) { - // return [start]; + // We could simply `return [start]` here, + // but there is no standardized "spacing" + // for locator hashes. Pretend this hash + // is our tip. This is useful for getheaders + // when not using headers-first. hashes.push(start); top = chain.length - 1; } @@ -574,7 +628,7 @@ Chain.prototype.target = function target(last, block) { } // Back 2 weeks - first = this.byHeight(last.height - (interval - 1)); + first = this.index.entries[last.height - (interval - 1)]; if (!first) return 0; @@ -734,7 +788,7 @@ function ChainBlock(chain, data) { this.chain = chain; this.hash = data.hash; this.version = data.version; - // this.prevBlock = data.prevBlock; + this.prevBlock = data.prevBlock; this.ts = data.ts; this.bits = data.bits; this.height = data.height; @@ -753,9 +807,7 @@ ChainBlock.prototype.__defineGetter__('proof', function() { var target = utils.fromCompact(this.bits); if (target.isNeg() || target.cmpn(0) === 0) return new bn(0); - // May be faster: - // return new bn(1).shln(256).div(target.addn(1)); - return new bn(2).pow(new bn(256)).div(target.addn(1)); + return new bn(1).ushln(256).div(target.addn(1)); }); ChainBlock.prototype.getChainwork = function() { @@ -808,7 +860,7 @@ ChainBlock.prototype.toJSON = function() { // return [ // this.hash, // this.version, - // // this.prevBlock, + // this.prevBlock, // this.ts, // this.bits, // this.height @@ -816,7 +868,7 @@ ChainBlock.prototype.toJSON = function() { return { hash: this.hash, version: this.version, - // prevBlock: this.prevBlock, + prevBlock: this.prevBlock, ts: this.ts, bits: this.bits, height: this.height @@ -827,9 +879,10 @@ ChainBlock.fromJSON = function(chain, json) { // return new ChainBlock(chain, { // hash: json[0], // version: json[1], - // ts: json[2], - // bits: json[3], - // height: json[4] + // prevBlock: json[2], + // ts: json[3], + // bits: json[4], + // height: json[5] // }); return new ChainBlock(chain, json); };