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),
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);
};