handle chain forks better.

This commit is contained in:
Christopher Jeffrey 2016-01-06 00:14:51 -08:00
parent 680bf01e98
commit ee97c7ed12

View File

@ -58,7 +58,7 @@ function Chain(options) {
{ {
hash: utils.toHex(network.genesis._hash), hash: utils.toHex(network.genesis._hash),
version: network.genesis.version, version: network.genesis.version,
// prevBlock: utils.toHex(network.genesis.prevBlock), prevBlock: utils.toHex(network.genesis.prevBlock),
ts: network.genesis.ts, ts: network.genesis.ts,
bits: network.genesis.bits, bits: network.genesis.bits,
height: 0 height: 0
@ -158,6 +158,11 @@ Chain.prototype._addIndex = function _addIndex(entry) {
if (checkpoint) { if (checkpoint) {
this.emit('checkpoint', entry.height, entry.hash, checkpoint); this.emit('checkpoint', entry.height, entry.hash, checkpoint);
if (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); this.emit('fork', entry.height, entry.hash, checkpoint);
return Chain.codes.badCheckpoint; return Chain.codes.badCheckpoint;
} }
@ -177,12 +182,18 @@ Chain.prototype._addIndex = function _addIndex(entry) {
}; };
Chain.prototype.resetLastCheckpoint = function resetLastCheckpoint(height) { 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) assert(index >= 0);
lastHeight = 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) { Chain.prototype.resetHeight = function resetHeight(height) {
@ -191,14 +202,21 @@ Chain.prototype.resetHeight = function resetHeight(height) {
assert(height < this.index.entries.length); assert(height < this.index.entries.length);
// Nothing to do.
if (height === this.index.entries.length - 1) if (height === this.index.entries.length - 1)
return; 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.map = {};
this.orphan.bmap = {}; this.orphan.bmap = {};
this.orphan.count = 0; this.orphan.count = 0;
this.orphan.size = 0; this.orphan.size = 0;
// Rebuild the index from our new (lower) height.
this.index.entries.length = height + 1; this.index.entries.length = height + 1;
this.index.heights = this.index.entries.reduce(function(out, entry) { this.index.heights = this.index.entries.reduce(function(out, entry) {
if (!self.options.fullNode) { if (!self.options.fullNode) {
if (!entry) if (!entry)
@ -207,6 +225,7 @@ Chain.prototype.resetHeight = function resetHeight(height) {
out[entry.hash] = entry.height; out[entry.hash] = entry.height;
return out; return out;
}, {}); }, {});
this.index.hashes.length = height + 1; this.index.hashes.length = height + 1;
if (!this.options.fullNode) if (!this.options.fullNode)
@ -214,14 +233,20 @@ Chain.prototype.resetHeight = function resetHeight(height) {
else else
this.index.count = height + 1; this.index.count = height + 1;
// Set and emit our new (old) tip.
this.tip = this.index.entries[this.index.entries.length - 1]; this.tip = this.index.entries[this.index.entries.length - 1];
this.emit('tip', this.tip); 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 = Math.min(
this.index.lastTs, this.index.lastTs,
this.index.entries[this.index.entries.length - 1].ts this.index.entries[this.index.entries.length - 1].ts
); );
// Delete all the blocks now above us.
ahead.forEach(function(entry) { ahead.forEach(function(entry) {
if (!self.options.fullNode) { if (!self.options.fullNode) {
if (!entry) if (!entry)
@ -239,6 +264,10 @@ Chain.prototype.resetTime = function resetTime(ts) {
}; };
Chain.prototype.add = function add(block, peer) { Chain.prototype.add = function add(block, peer) {
var initial = block;
var code = Chain.codes.unchanged;
var hash, prevHash, prevHeight, entry;
if (this.loading) { if (this.loading) {
this.once('load', function() { this.once('load', function() {
this.add(block); this.add(block);
@ -246,33 +275,35 @@ Chain.prototype.add = function add(block, peer) {
return; return;
} }
var initial = block;
var code = Chain.codes.unchanged;
var hash, prev, i, entry;
for (;;) { 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()) { if (block === initial && !block.verify()) {
code = Chain.codes.invalid; code = Chain.codes.invalid;
break; break;
} }
hash = block.hash('hex'); hash = block.hash('hex');
prev = block.prevBlock; prevHash = block.prevBlock;
// If the block is already known to be an orphan // If the block is already known to be
if (this.orphan.map[prev]) { // an orphan, ignore it.
if (this.orphan.map[prevHash]) {
code = Chain.codes.knownOrphan; code = Chain.codes.knownOrphan;
break; 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 previous block wasn't ever seen,
if (i == null) { // add it current to orphans and break.
if (prevHeight == null) {
this.orphan.count++; this.orphan.count++;
this.orphan.size += block.size(); this.orphan.size += block.size();
this.orphan.map[prev] = block; this.orphan.map[prevHash] = block;
this.orphan.bmap[hash] = block; this.orphan.bmap[hash] = block;
code = Chain.codes.newOrphan; code = Chain.codes.newOrphan;
break; break;
@ -281,50 +312,67 @@ Chain.prototype.add = function add(block, peer) {
entry = new ChainBlock(this, { entry = new ChainBlock(this, {
hash: hash, hash: hash,
version: block.version, version: block.version,
// prevBlock: prev, prevBlock: prevHash,
ts: block.ts, ts: block.ts,
bits: block.bits, 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) { if (this.index.hashes[entry.height] !== hash) {
assert(this.index.heights[entry.hash] == null); 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.index.hashes[entry.height]) {
if (this.tip.chainwork.cmp(entry.chainwork) < 0) { // The tip has more chainwork, it is a
if (!block.postVerify()) { // higher height than the entry. This is
throw new Error; // not an alternate tip. Ignore it.
//code = Chain.codes.invalid; if (this.tip.chainwork.cmp(entry.chainwork) > 0) {
//break; code = Chain.codes.unchanged;
}
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);
break; 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()) { if (!block.postVerify()) {
throw new Error; throw new Error;
//code = Chain.codes.invalid; //code = Chain.codes.invalid;
//break; //break;
} }
// Attempt to add block to the chain index.
code = this._addIndex(entry); code = this._addIndex(entry);
// Result should never be `unchanged` since
// we already verified there were no
// duplicate heights, etc.
assert(code !== Chain.codes.unchanged); 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) if (code !== Chain.codes.okay)
break; break;
// Emit our block (and potentially resolved
// orphan) so the programmer can save it.
this.emit('block', block, peer); this.emit('block', block, peer);
this.emit('entry', entry); this.emit('entry', entry);
if (block !== initial) if (block !== initial)
@ -337,7 +385,7 @@ Chain.prototype.add = function add(block, peer) {
if (!this.orphan.map[hash]) if (!this.orphan.map[hash])
break; break;
// We have orphan child for this block - add it to chain // An orphan chain was found, start resolving.
block = this.orphan.map[hash]; block = this.orphan.map[hash];
delete this.orphan.bmap[block.hash('hex')]; delete this.orphan.bmap[block.hash('hex')];
delete this.orphan.map[hash]; delete this.orphan.map[hash];
@ -345,7 +393,8 @@ Chain.prototype.add = function add(block, peer) {
this.orphan.size -= block.size(); 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) { if (this.orphan.size > 20971520) {
this.orphan.map = {}; this.orphan.map = {};
this.orphan.bmap = {}; this.orphan.bmap = {};
@ -353,7 +402,8 @@ Chain.prototype.add = function add(block, peer) {
this.orphan.size = 0; 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.options.fullNode) {
// if (this.size() > 100000) // if (this.size() > 100000)
// this.compact(); // this.compact();
@ -477,7 +527,11 @@ Chain.prototype.locatorHashes = function locatorHashes(start) {
if (typeof start === 'string') { if (typeof start === 'string') {
top = this.index.heights[start]; top = this.index.heights[start];
if (top == null) { 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); hashes.push(start);
top = chain.length - 1; top = chain.length - 1;
} }
@ -574,7 +628,7 @@ Chain.prototype.target = function target(last, block) {
} }
// Back 2 weeks // Back 2 weeks
first = this.byHeight(last.height - (interval - 1)); first = this.index.entries[last.height - (interval - 1)];
if (!first) if (!first)
return 0; return 0;
@ -734,7 +788,7 @@ function ChainBlock(chain, data) {
this.chain = chain; this.chain = chain;
this.hash = data.hash; this.hash = data.hash;
this.version = data.version; this.version = data.version;
// this.prevBlock = data.prevBlock; this.prevBlock = data.prevBlock;
this.ts = data.ts; this.ts = data.ts;
this.bits = data.bits; this.bits = data.bits;
this.height = data.height; this.height = data.height;
@ -753,9 +807,7 @@ ChainBlock.prototype.__defineGetter__('proof', function() {
var target = utils.fromCompact(this.bits); var target = utils.fromCompact(this.bits);
if (target.isNeg() || target.cmpn(0) === 0) if (target.isNeg() || target.cmpn(0) === 0)
return new bn(0); return new bn(0);
// May be faster: return new bn(1).ushln(256).div(target.addn(1));
// return new bn(1).shln(256).div(target.addn(1));
return new bn(2).pow(new bn(256)).div(target.addn(1));
}); });
ChainBlock.prototype.getChainwork = function() { ChainBlock.prototype.getChainwork = function() {
@ -808,7 +860,7 @@ ChainBlock.prototype.toJSON = function() {
// return [ // return [
// this.hash, // this.hash,
// this.version, // this.version,
// // this.prevBlock, // this.prevBlock,
// this.ts, // this.ts,
// this.bits, // this.bits,
// this.height // this.height
@ -816,7 +868,7 @@ ChainBlock.prototype.toJSON = function() {
return { return {
hash: this.hash, hash: this.hash,
version: this.version, version: this.version,
// prevBlock: this.prevBlock, prevBlock: this.prevBlock,
ts: this.ts, ts: this.ts,
bits: this.bits, bits: this.bits,
height: this.height height: this.height
@ -827,9 +879,10 @@ ChainBlock.fromJSON = function(chain, json) {
// return new ChainBlock(chain, { // return new ChainBlock(chain, {
// hash: json[0], // hash: json[0],
// version: json[1], // version: json[1],
// ts: json[2], // prevBlock: json[2],
// bits: json[3], // ts: json[3],
// height: json[4] // bits: json[4],
// height: json[5]
// }); // });
return new ChainBlock(chain, json); return new ChainBlock(chain, json);
}; };