From 5f67f78170231df713a9b8c7eee684564b22b6ce Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 19 Feb 2016 01:42:59 -0800 Subject: [PATCH] chain fixes. --- lib/bcoin/chain.js | 154 +++++++++++++++++++++++++------------------ lib/bcoin/chaindb.js | 13 +++- lib/bcoin/pool.js | 2 +- 3 files changed, 102 insertions(+), 67 deletions(-) diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 448358f3..e3fdc7db 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -291,8 +291,6 @@ Chain.prototype._preload = function _preload(callback) { // Verify the block headers. We don't want to // trust an external centralized source completely. - // For very paranoid but slower validation: - // if (!block.verify() || !block.verifyContext()) { if (!block.verify()) { start = Math.max(0, height - 2); stream.destroy(); @@ -305,14 +303,18 @@ Chain.prototype._preload = function _preload(callback) { delete entry.chainwork; entry.chainwork = entry.getChainwork(); - // Skip the genesis block in case - // it ends up being corrupt. - if (height === 0) { - height++; - return; + // Make sure the genesis block is correct. + if (height === 0 && entry.hash !== network.genesis.hash) { + stream.destroy(); + return callback(new Error('Bad genesis block.'), 0); } - self.db.save(entry); + // Filthy hack to avoid writing + // redundant blocks to disk! + if (height < chainHeight) + self.db._populate(entry); + else + self.db.save(entry); height++; @@ -402,6 +404,17 @@ Chain.prototype._verify = function _verify(block, prev) { return false; } + // For some reason bitcoind has p2sh in the + // mandatory flags by default, when in reality + // it wasn't activated until march 30th 2012. + // The first p2sh output and redeem script + // appeared on march 7th 2012, only it did + // not have a signature. See: + // https://blockchain.info/tx/6a26d2ecb67f27d1fa5524763b49029d7106e91e3cc05743073461a719776192 + // https://blockchain.info/tx/9c08a4d78931342b37fd5f72900fb9983087e6f46c4a097d8a1f52c74e28eaf6 + if (block.ts < constants.block.bip16time) + flags &= ~constants.flags.VERIFY_P2SH; + // Only allow version 2 blocks (coinbase height) // once the majority of blocks are using it. if (block.version < 2 && prev.isOutdated(2)) { @@ -553,8 +566,10 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac // If we are an ancestor of a checkpoint, we can // skip the input verification. - // if (height < network.checkpoints.lastHeight && !network.checkpoints[height]) - // return callback(null, true); + if (this.options.useCheckpoints) { + if (height < network.checkpoints.lastHeight && !network.checkpoints[height]) + return callback(null, true); + } this._fillBlock(block, function(err) { var i, j, input, hash; @@ -579,9 +594,8 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac utils.debug('Block is using spent inputs: %s (tx: %s, output: %s)', block.rhash, tx.rhash, input.prevout.rhash + '/' + input.prevout.index); - throw new Error('Spent inputs: ' - + utils._inspect(input, false) - + JSON.stringify(input, null, 2)); + if (height < network.checkpoints.lastHeight) + throw new Error('BUG: Spent inputs in historical data!'); return callback(null, false); } @@ -589,15 +603,15 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac if (!tx.verify(j, true, flags)) { utils.debug('Block has invalid inputs: %s (%s/%d)', block.rhash, tx.rhash, j); - utils.debug('Signature Hash: %s', utils.toHex(tx.signatureHash(j, input.output.script, 'all'))); utils.debug(input); - utils.debug('raw: %s', utils.toHex(input.output.script._raw || [])); - utils.debug('encoded: %s', utils.toHex(bcoin.script.encode(input.output.script))); - throw new Error('Bad inputs: ' - + utils._inspect(input, false) - + JSON.stringify(input, null, 2) - + '\n' - + utils.toHex(tx.signatureHash(j, input.output.script, 'all'))); + utils.debug('Signature Hash: %s', + utils.toHex(tx.signatureHash(j, input.output.script, 'all'))); + utils.debug('Raw Script: %s', + utils.toHex(input.output.script._raw || [])); + utils.debug('Reserialized Script: %s', + utils.toHex(bcoin.script.encode(input.output.script))); + if (height < network.checkpoints.lastHeight) + throw new Error('BUG: Bad inputs in historical data!'); return callback(null, false); } } @@ -671,7 +685,6 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) { Chain.prototype.resetHeight = function resetHeight(height) { var self = this; - var count = this.db.count(); var i, existing; assert(!this.resetting); @@ -695,7 +708,6 @@ Chain.prototype.resetHeight = function resetHeight(height) { Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { var self = this; - var count = this.db.count(); var i, lock; assert(!this.resetting); @@ -865,9 +877,6 @@ Chain.prototype.add = function add(initial, peer, callback) { assert(!this.loading); - if (this.resetting || this.reverting || this.syncing) - return callback(); - if (this.locked) { this.pending.push([initial, peer, callback]); this.pendingBlocks[initial.hash('hex')] = true; @@ -879,6 +888,9 @@ Chain.prototype.add = function add(initial, peer, callback) { return; } + if (this.resetting || this.reverting || this.syncing) + return callback(); + this.locked = true; (function next(block) { @@ -911,7 +923,7 @@ Chain.prototype.add = function add(initial, peer, callback) { if (block === initial && !block.verify()) { self.invalid[hash] = true; self.emit('invalid', block, { - height: prevHeight + 1, + height: prevHeight === -1 ? -1 : prevHeight + 1, hash: hash, seen: false, chain: false @@ -927,23 +939,32 @@ Chain.prototype.add = function add(initial, peer, callback) { // reset the orphans and find a new peer. if (orphan.hash('hex') !== hash) { self.emit('purge', self.orphan.count, self.orphan.size, peer); + self.orphan.map = {}; self.orphan.bmap = {}; self.orphan.count = 0; self.orphan.size = 0; + self.emit('fork', block, { height: -1, expected: orphan.hash('hex'), received: hash, checkpoint: false }, peer); + + // Clear the queue. No telling what other + // blocks we're going to get after this. + self.pending.length = 0; + return done(); } + self.emit('orphan', block, { height: -1, hash: hash, seen: true }, peer); + return done(); } @@ -956,7 +977,7 @@ Chain.prototype.add = function add(initial, peer, callback) { // If previous block wasn't ever seen, // add it current to orphans and break. - if (prevHeight == null) { + if (prevHeight === -1) { self.orphan.count++; self.orphan.size += block.getSize(); self.orphan.map[prevHash] = block; @@ -981,13 +1002,7 @@ Chain.prototype.add = function add(initial, peer, callback) { height: prevHeight + 1 }); - // Fork at checkpoint - // 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. + // Verify the checkpoint. checkpoint = network.checkpoints[entry.height]; if (checkpoint) { self.emit('checkpoint', block, { @@ -995,18 +1010,25 @@ Chain.prototype.add = function add(initial, peer, callback) { hash: entry.hash, checkpoint: checkpoint }, peer); + + // 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 (entry.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); self.emit('fork', block, { height: entry.height, expected: network.checkpoints[entry.height], received: entry.hash, checkpoint: true }, peer); + + // Clear the queue. No telling what other + // blocks we're going to get after this. + self.pending.length = 0; + return done(); } } @@ -1035,19 +1057,25 @@ Chain.prototype.add = function add(initial, peer, callback) { // 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. - assert(self.db.getHeight(entry.hash) == null); + assert(self.db.getHeight(entry.hash) === -1); // The tip has more chainwork, it is a // higher height than the entry. This is // not an alternate tip. Ignore it. - if (self.tip.chainwork.cmp(entry.chainwork) > 0) + if (existing.chainwork.cmp(entry.chainwork) > 0) return done(); + // NOTE: We should do contextual verification + // here if we were a fullnode that actually + // stored multiple chains, but since we backoff, + // we can ignore that until the shorter chain + // stops being propogated. + // The block has equal chainwork (an // alternate tip). Reset the chain, find // a new peer, and wait to see who wins. - // return self.revertHeight(existing.height - 1, function(err) { - return self._revertLast(existing, function(err, existingBlock) { + // self.revertHeight(existing.height - 1, function(err) { + self._revertLast(existing, function(err, existingBlock) { if (err) return done(err); @@ -1058,13 +1086,17 @@ Chain.prototype.add = function add(initial, peer, callback) { checkpoint: false }, peer); + // Clear the queue. No telling what other + // blocks we're going to get after this. + self.pending.length = 0; + return done(); }); }); } // Add entry if we do not have it. - assert(self.db.getHeight(entry.hash) == null); + assert(self.db.getHeight(entry.hash) === -1); // Lookup previous entry. // We can do this synchronously: @@ -1201,7 +1233,7 @@ Chain.prototype.has = function has(hash) { }; Chain.prototype.byHeight = function byHeight(height) { - if (height == null) + if (height < 0 || height == null) return; return this.db.get(height); }; @@ -1217,7 +1249,7 @@ Chain.prototype.byHash = function byHash(hash) { Chain.prototype.byTime = function byTime(ts) { var start = 0; - var end = this.db.count(); + var end = this.height + 1; var pos, delta, entry; if (ts >= this.tip.ts) @@ -1324,14 +1356,14 @@ Chain.prototype.getLocator = function getLocator(start) { if (typeof start === 'string') { top = this.db.getHeight(start); - if (top == null) { + if (top === -1) { // 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 = this.db.count() - 1; + top = this.height; } } else if (typeof start === 'number') { top = start; @@ -1362,7 +1394,7 @@ Chain.prototype.getLocatorAsync = function getLocatorAsync(start, callback) { var hashes = []; var top = this.height; var step = 1; - var i, called; + var i, called, pending; if (start) { if (utils.isBuffer(start)) @@ -1373,19 +1405,23 @@ Chain.prototype.getLocatorAsync = function getLocatorAsync(start, callback) { if (typeof start === 'string') { top = this.db.getHeight(start); - if (top == null) { + if (top === -1) { // 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 = this.db.count() - 1; + top = this.height; } } else if (typeof start === 'number') { top = start; } + assert(this.db.has(top)); + + callback = utils.asyncify(callback); + function done(err) { if (called) return; @@ -1398,14 +1434,6 @@ Chain.prototype.getLocatorAsync = function getLocatorAsync(start, callback) { return callback(null, hashes); } - try { - assert(this.db.has(top)); - } catch (e) { - return done(new Error('Potential reset.')); - } - - var pending; - i = top; for (;;) { hashes.push(i); @@ -1463,11 +1491,7 @@ Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { }; Chain.prototype.getHeight = function getHeight(hash) { - var height = this.db.getHeight(hash); - if (height == null) - return -1; - - return height; + return this.db.getHeight(hash); }; Chain.prototype.getNextBlock = function getNextBlock(hash) { diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 7ddcc111..1fdd699f 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -242,7 +242,12 @@ ChainDB.prototype.cache = function cache(entry) { }; ChainDB.prototype.getHeight = function getHeight(hash) { - return this.heightLookup[hash]; + var height = this.heightLookup[hash]; + + if (height == null) + return -1; + + return height; }; ChainDB.prototype._populate = function _populate(entry) { @@ -344,6 +349,9 @@ ChainDB.prototype.save = function save(entry, callback) { ChainDB.prototype.saveSync = function saveSync(entry) { var raw, offset; + assert(entry.height >= 0); + assert(entry.height * BLOCK_SIZE === this.size); + // Cache the past 1001 blocks in memory // (necessary for isSuperMajority) this.cache(entry); @@ -362,6 +370,9 @@ ChainDB.prototype.saveAsync = function saveAsync(entry, callback) { callback = utils.asyncify(callback); + assert(entry.height >= 0); + assert(entry.height * BLOCK_SIZE === this.size); + // Cache the past 1001 blocks in memory // (necessary for isSuperMajority) this.cache(entry); diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index dc7278af..8c25c850 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -687,7 +687,7 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { self.chain.orphan.count, self.request.activeBlocks, peer._blockQueue.length, - self.chain.currentTarget(), + self.chain.getCurrentTarget(), self.peers.all.length, self.chain.pending.length, self.chain.bestHeight);