From 311b9841fb6f73414c159f50b2285e4b6167b9b5 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 30 Nov 2016 16:00:22 -0800 Subject: [PATCH] chain: more refactoring. --- lib/blockchain/chain.js | 164 ++++++++++++++++++-------------- lib/http/rpc.js | 4 +- lib/primitives/abstractblock.js | 34 +++++++ lib/primitives/memblock.js | 1 + test/block-test.js | 20 ++-- 5 files changed, 142 insertions(+), 81 deletions(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index cf8c9aca..d8d3dcea 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -15,6 +15,7 @@ var constants = require('../protocol/constants'); var util = require('../utils/util'); var btcutils = require('../btc/utils'); var Locker = require('../utils/locker'); +var LRU = require('../utils/lru'); var ChainEntry = require('./chainentry'); var CoinView = require('./coinview'); var assert = require('assert'); @@ -80,7 +81,7 @@ function Chain(options) { this.currentBlock = null; this.orphanLimit = options.orphanLimit || (20 << 20); this.locker = new Locker(true); - this.invalid = {}; + this.invalid = new LRU(100); this.bestHeight = -1; this.tip = null; this.height = -1; @@ -133,17 +134,17 @@ Chain.prototype._init = function _init() { block.rhash, entry.height); }); - this.on('checkpoint', function(block, height) { + this.on('checkpoint', function(hash, height) { self.logger.debug('Hit checkpoint block %s (%d).', - block.rhash, height); + util.revHex(hash), height); }); - this.on('fork', function(block, height, expected) { + this.on('fork', function(hash, height, expected) { self.logger.warning( 'Fork at height %d: expected=%s received=%s', height, util.revHex(expected), - block.rhash + util.revHex(hash) ); }); @@ -901,7 +902,7 @@ Chain.prototype.reconnect = co(function* reconnect(entry) { } catch (e) { if (e.type === 'VerifyError') { if (!e.malleated) - this.invalid[entry.hash] = true; + this.setInvalid(entry.hash); this.emit('invalid', block, entry.height); } throw e; @@ -967,7 +968,7 @@ Chain.prototype.setBestChain = co(function* setBestChain(entry, block, prev) { if (e.type === 'VerifyError') { if (!e.malleated) - this.invalid[entry.hash] = true; + this.setInvalid(entry.hash); this.emit('invalid', block, entry.height); } @@ -1006,7 +1007,7 @@ Chain.prototype.saveAlternate = co(function* saveAlternate(entry, block, prev) { if (e.type === 'VerifyError') { if (!e.malleated) - this.invalid[entry.hash] = true; + this.setInvalid(entry.hash); this.emit('invalid', block, entry.height); } @@ -1210,7 +1211,7 @@ Chain.prototype.add = co(function* add(block) { Chain.prototype._add = co(function* add(block) { var ret = new VerifyResult(); var initial = true; - var hash, height, orphan, entry, prev; + var hash, height, entry, prev; while (block) { hash = block.hash('hex'); @@ -1222,13 +1223,6 @@ Chain.prototype._add = co(function* add(block) { if (hash === this.network.genesis.hash) break; - // Do not revalidate known invalid blocks. - if (this.invalid[hash] || this.invalid[block.prevBlock]) { - this.emit('invalid', block, block.getCoinbaseHeight()); - this.invalid[hash] = true; - throw new VerifyError(block, 'duplicate', 'duplicate', 100); - } - // Do we already have this block? if (this.hasPending(hash)) { this.emit('exists', block, block.getCoinbaseHeight()); @@ -1237,42 +1231,22 @@ Chain.prototype._add = co(function* add(block) { // If the block is already known to be // an orphan, ignore it. - // if (this.hasOrphan(hash)) { - // this.emit('orphan', block, block.getCoinbaseHeight()); - // throw new VerifyError(block, 'duplicate', 'duplicate', 0); - // } - - // The orphan chain may have forked. - // if (this.isOrphanFork(block)) - // throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); - - // If the block is already known to be - // an orphan, ignore it. - orphan = this.orphan.map[block.prevBlock]; - if (orphan) { - if (orphan.hash('hex') !== hash) { - this.emit('fork', block, - block.getCoinbaseHeight(), - orphan.hash('hex')); - - this.resolveOrphan(block.prevBlock); - this.storeOrphan(block); - - throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); - } - + if (this.seenOrphan(hash)) { this.emit('orphan', block, block.getCoinbaseHeight()); - throw new VerifyError(block, 'duplicate', 'duplicate', 0); } + // Do not revalidate known invalid blocks. + if (this.hasInvalid(hash, block)) { + this.emit('invalid', block, block.getCoinbaseHeight()); + throw new VerifyError(block, 'duplicate', 'duplicate', 100); + } + // Validate the block we want to add. // This is only necessary for new // blocks coming in, not the resolving // orphans. - if (initial && !block.verify(ret)) { - if (ret.reason === 'high-hash') - this.invalid[hash] = true; + if (!block.verify(ret)) { this.emit('invalid', block, block.getCoinbaseHeight()); throw new VerifyError(block, 'invalid', ret.reason, ret.score); } @@ -1438,31 +1412,6 @@ Chain.prototype.finish = function finish(block, entry) { time); }; -/** - * Test whether a block is a on a forked orphan chain. - * @private - * @param {Block} block - * @returns {Boolean} - */ - -Chain.prototype.isOrphanFork = function isOrphanFork(block) { - var orphan = this.orphan.map[block.prevBlock]; - - if (!orphan) - return false; - - assert(orphan.hash('hex') !== hash); - - this.emit('fork', block, - block.getCoinbaseHeight(), - orphan.hash('hex')); - - this.resolveOrphan(block.prevBlock); - this.storeOrphan(block); - - return true; -}; - /** * Verify a block hash and height against the checkpoints. * @private @@ -1483,7 +1432,7 @@ Chain.prototype.verifyCheckpoint = function verifyCheckpoint(hash, height) { return true; if (hash === checkpoint) { - this.emit('checkpoint', block, height); + this.emit('checkpoint', hash, height); return true; } @@ -1495,11 +1444,44 @@ Chain.prototype.verifyCheckpoint = function verifyCheckpoint(hash, height) { this.purgeOrphans(); - this.emit('fork', block, height, checkpoint); + this.emit('fork', hash, height, checkpoint); return false; }; +/** + * Verify we do not already have an orphan. + * Throw if there is an orphan fork. + * @private + * @param {Block} block + * @returns {Boolean} + * @throws {VerifyError} + */ + +Chain.prototype.seenOrphan = function seenOrphan(block) { + var orphan = this.orphan.map[block.prevBlock]; + var hash; + + if (!orphan) + return false; + + hash = block.hash('hex'); + + // The orphan chain forked. + if (orphan.hash('hex') !== hash) { + this.emit('fork', hash, + block.getCoinbaseHeight(), + orphan.hash('hex')); + + this.resolveOrphan(block.prevBlock); + this.storeOrphan(block); + + throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); + } + + return true; +}; + /** * Store an orphan. * @private @@ -1624,6 +1606,46 @@ Chain.prototype.pruneOrphans = function pruneOrphans() { return true; }; +/** + * Test whether an invalid block hash has been seen. + * @private + * @param {Hash} hash + * @param {Block} block + * @returns {Boolean} + */ + +Chain.prototype.hasInvalid = function hasInvalid(hash, block) { + if (this.invalid.has(hash)) + return true; + + if (this.invalid.has(block.prevBlock)) { + this.setInvalid(hash); + return true; + } + + return false; +}; + +/** + * Mark a block as invalid. + * @private + * @param {Hash} hash + */ + +Chain.prototype.setInvalid = function setInvalid(hash) { + this.invalid.set(hash, true); +}; + +/** + * Forget an invalid block hash. + * @private + * @param {Hash} hash + */ + +Chain.prototype.removeInvalid = function removeInvalid(hash) { + this.invalid.remove(hash); +}; + /** * Test the chain to see if it has a block, orphan, or pending block. * @param {Hash} hash diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 5b90bb80..0f911779 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -2604,7 +2604,7 @@ RPC.prototype.invalidateblock = function invalidateblock(args) { if (!hash) return Promise.reject(new RPCError('Block not found.')); - this.chain.invalid[hash] = true; + this.chain.setInvalid(hash); return Promise.resolve(); }; @@ -2620,7 +2620,7 @@ RPC.prototype.reconsiderblock = function reconsiderblock(args) { if (!hash) return Promise.reject(new RPCError('Block not found.')); - delete this.chain.invalid[hash]; + this.chain.removeInvalid(hash); return Promise.resolve(); }; diff --git a/lib/primitives/abstractblock.js b/lib/primitives/abstractblock.js index ced1cccd..ebf7dc79 100644 --- a/lib/primitives/abstractblock.js +++ b/lib/primitives/abstractblock.js @@ -54,6 +54,7 @@ function AbstractBlock(options) { this.mutable = false; this._valid = null; + this._validHeaders = null; this._hash = null; this._hhash = null; this._size = null; @@ -199,6 +200,18 @@ AbstractBlock.prototype.verify = function verify(ret) { return valid; }; +/** + * Verify the block. + * @private + * @param {Object?} ret - Return object, may be + * set with properties `reason` and `score`. + * @returns {Boolean} + */ + +AbstractBlock.prototype._verify = function verify(ret) { + throw new Error('Abstract method.'); +}; + /** * Verify the block headers (called by `verify()` in * all objects which inherit from AbstractBlock). @@ -208,6 +221,27 @@ AbstractBlock.prototype.verify = function verify(ret) { */ AbstractBlock.prototype.verifyHeaders = function verifyHeaders(ret) { + var valid = this._validHeaders; + + if (valid == null) { + valid = this._verifyHeaders(ret); + if (!this.mutable) + this._validHeaders = valid; + } + + return valid; +}; + +/** + * Verify the block headers (called by `verify()` in + * all objects which inherit from AbstractBlock). + * @private + * @param {Object?} ret - Return object, may be + * set with properties `reason` and `score`. + * @returns {Boolean} + */ + +AbstractBlock.prototype._verifyHeaders = function verifyHeaders(ret) { if (!ret) ret = new VerifyResult(); diff --git a/lib/primitives/memblock.js b/lib/primitives/memblock.js index 3ca10296..22464b9c 100644 --- a/lib/primitives/memblock.js +++ b/lib/primitives/memblock.js @@ -205,6 +205,7 @@ MemBlock.prototype.toBlock = function toBlock() { var block = Block.fromRaw(this.raw); block._hash = this._hash; block._cbHeight = this.coinbaseHeight; + block._validHeaders = this._validHeaders; this.raw = null; return block; }; diff --git a/test/block-test.js b/test/block-test.js index 25a57254..af97b1a7 100644 --- a/test/block-test.js +++ b/test/block-test.js @@ -164,12 +164,14 @@ describe('Block', function() { var block2 = new bcoin.block(block); block2.hash(); block2.merkleRoot = constants.NULL_HASH; - delete block2._valid; + block2._valid = null; + block2._validHeaders = null; var ret = {}; assert(!block2.verify(ret)); assert.equal(ret.reason, 'bad-txnmrklroot'); - delete block2._valid; - delete block2._hash; + block2._valid = null; + block2._validHeaders = null; + block2._hash = null; block2.merkleRoot = block.merkleRoot; assert(block2.verify()); }); @@ -181,9 +183,10 @@ describe('Block', function() { var ret = {}; assert(!mblock2.verify(ret)); assert.equal(ret.reason, 'bad-txnmrklroot'); - delete mblock2._validPartial; - delete mblock2._valid; - delete mblock2._hash; + mblock2._valid = null; + mblock2._validHeaders = null; + mblock2._validPartial = null; + mblock2._hash = null; mblock2.merkleRoot = mblock.merkleRoot; assert(mblock2.verify()); }); @@ -195,8 +198,9 @@ describe('Block', function() { var ret = {}; assert(!block2.verify(ret)); assert.equal(ret.reason, 'high-hash'); - delete block2._valid; - delete block2._hash; + block2._valid = null; + block2._validHeaders = null; + block2._hash = null; block2.bits = block.bits; assert(block2.verify()); });