From 60a7f5ea29532c07c945f2750c386931f0eb7b94 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 4 Sep 2015 11:32:04 -0400 Subject: [PATCH 1/2] optimizations for handling reorgs better --- lib/services/db.js | 189 +++++++++++++++------------------------------ 1 file changed, 61 insertions(+), 128 deletions(-) diff --git a/lib/services/db.js b/lib/services/db.js index 9a0e117a..b3dc86c6 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -48,10 +48,6 @@ function DB(options) { this._setDataPath(); - this.cache = { - hashes: {}, // dictionary of hash -> prevHash - chainHashes: {} - }; this.lastSavedMetadata = null; this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance @@ -133,7 +129,6 @@ DB.prototype.start = function(callback) { self.tip = tip; self.tip.__height = metadata.tipHeight; - self.cache = metadata.cache; self.sync(); self.emit('ready'); setImmediate(callback); @@ -311,7 +306,6 @@ DB.prototype.saveMetadata = function(callback) { var metadata = { tip: self.tip ? self.tip.hash : null, tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, - cache: self.cache }; self.lastSavedMetadata = new Date(); @@ -406,76 +400,6 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { ); }; -/** - * Will get an array of hashes all the way to the genesis block for - * the chain based on "block hash" as the tip. - * - * @param {String} block hash - a block hash - * @param {Function} callback - A function that accepts: Error and Array of hashes - */ -DB.prototype.getHashes = function getHashes(tipHash, callback) { - var self = this; - - $.checkArgument(utils.isHash(tipHash)); - - var hashes = []; - var depth = 0; - - function getHashAndContinue(err, hash) { - /* jshint maxstatements: 20 */ - - if (err) { - return callback(err); - } - - depth++; - - hashes.unshift(hash); - - if (hash === self.genesis.hash) { - // Stop at the genesis block - self.cache.chainHashes[tipHash] = hashes; - - callback(null, hashes); - } else if(self.cache.chainHashes[hash]) { - hashes.shift(); - hashes = self.cache.chainHashes[hash].concat(hashes); - self.cache.chainHashes[tipHash] = hashes; - if(hash !== tipHash) { - delete self.cache.chainHashes[hash]; - } - callback(null, hashes); - } else { - // Continue with the previous hash - // check cache first - var prevHash = self.cache.hashes[hash]; - if(prevHash) { - // Don't let the stack get too deep. Otherwise we will crash. - if(depth >= MAX_STACK_DEPTH) { - depth = 0; - return setImmediate(function() { - getHashAndContinue(null, prevHash); - }); - } else { - return getHashAndContinue(null, prevHash); - } - } else { - // do a db call if we don't have it - self.getPrevHash(hash, function(err, prevHash) { - if(err) { - return callback(err); - } - - return getHashAndContinue(null, prevHash); - }); - } - } - } - - getHashAndContinue(null, tipHash); - -}; - /** * This function will find the common ancestor between the current chain and a forked block, * by moving backwards from the forked block until it meets the current chain. @@ -486,41 +410,60 @@ DB.prototype.findCommonAncestor = function(block, done) { var self = this; - // The current chain of hashes will likely already be available in a cache. - self.getHashes(self.tip.hash, function(err, currentHashes) { - if (err) { - done(err); + var mainPosition = self.tip.hash; + var forkPosition = block.hash; + + var mainHashesMap = {}; + var forkHashesMap = {}; + + mainHahesMap[mainPosition] = true; + forkHashesMap[forkPosition] = true; + + var commonAncestor = null; + + async.whilst( + function() { + return !commonAncestor; + }, + function(next) { + if(mainPosition) { + var mainBlockIndex = self.node.services.bitcoind.getBlockIndex(mainTip); + if(mainBlockIndex && mainBlockIndex.prevHash) { + mainHashesMap[mainBlockIndex.prevHash] = true; + mainPosition = mainBlockIndex.prevHash; + } else { + mainPosition = null; + } + } + + if(forkPosition) { + var forkBlockIndex = self.node.services.bitcoind.getBlockIndex(forkTip); + if(forkBlockIndex && forkBlockIndex.prevHash) { + forkHashesMap[forkBlockIndex.prevHash] = true; + forkPosition = forkBlockIndex.prevHash; + } else { + forkPosition = null; + } + } + + if(forkPosition && mainHashesMap[forkPosition]) { + commonAncestor = forkPosition; + } + + if(mainPosition && forkHashesMap[mainPosition]) { + commonAncestor = mainPosition; + } + + if(!mainPosition && !forkPosition) { + return next(new Error('Unknown common ancestor')); + } + + setImmediate(next); + }, + function(err) { + done(err, commonAncestor); } - - // Create a hash map for faster lookups - var currentHashesMap = {}; - var length = currentHashes.length; - for (var i = 0; i < length; i++) { - currentHashesMap[currentHashes[i]] = true; - } - - // TODO: expose prevHash as a string from bitcore - var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - // We only need to go back until we meet the main chain for the forked block - // and thus don't need to find the entire chain of hashes. - - while(ancestorHash && !currentHashesMap[ancestorHash]) { - var blockIndex = self.node.services.bitcoind.getBlockIndex(ancestorHash); - ancestorHash = blockIndex ? blockIndex.prevHash : null; - } - - // Hash map is no-longer needed, quickly let - // scavenging garbage collection know to cleanup - currentHashesMap = null; - - if (!ancestorHash) { - return done(new Error('Unknown common ancestor.')); - } - - done(null, ancestorHash); - - }); + ); }; /** @@ -620,28 +563,18 @@ DB.prototype.sync = function() { // Populate height block.__height = self.tip.__height + 1; - // Update cache.hashes - self.cache.hashes[block.hash] = prevHash; - - // Update cache.chainHashes - self.getHashes(block.hash, function(err, hashes) { + // Create indexes + self.connectBlock(block, function(err) { if (err) { return done(err); } - // Create indexes - self.connectBlock(block, function(err) { - if (err) { - return done(err); - } - self.tip = block; - log.debug('Saving metadata'); - self.saveMetadata(); - log.debug('Chain added block to main chain'); - self.emit('addblock', block); - setImmediate(done); - }); + self.tip = block; + log.debug('Saving metadata'); + self.saveMetadata(); + log.debug('Chain added block to main chain'); + self.emit('addblock', block); + setImmediate(done); }); - } else { // This block doesn't progress the current tip, so we'll attempt // to rewind the chain to the common ancestor of the block and From 50925d1e0fe651abd3118efd4c34ee51d70ac0d9 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Fri, 4 Sep 2015 13:52:41 -0400 Subject: [PATCH 2/2] Add tests for reorg improvement, and remove nolonger need code. --- lib/services/db.js | 35 ++++-------- test/services/db.unit.js | 113 +++++++++++++++------------------------ 2 files changed, 53 insertions(+), 95 deletions(-) diff --git a/lib/services/db.js b/lib/services/db.js index b3dc86c6..45537fa6 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -16,9 +16,6 @@ var errors = index.errors; var log = index.log; var Transaction = require('../transaction'); var Service = require('../service'); -var utils = require('../utils'); - -var MAX_STACK_DEPTH = 1000; /** * Represents the current state of the bitcoin blockchain. Other services @@ -48,9 +45,6 @@ function DB(options) { this._setDataPath(); - this.lastSavedMetadata = null; - this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance - this.levelupStore = leveldown; if (options.store) { this.levelupStore = options.store; @@ -121,18 +115,19 @@ DB.prototype.start = function(callback) { }); } else { - metadata.tip = metadata.tip; self.getBlock(metadata.tip, function(err, tip) { if(err) { return callback(err); } - self.tip = tip; - self.tip.__height = metadata.tipHeight; + var blockIndex = self.node.services.bitcoind.getBlockIndex(self.tip.hash); + if (!blockIndex) { + return callback(new Error('Could not get height for tip.')); + } + self.tip.__height = blockIndex.height; self.sync(); self.emit('ready'); setImmediate(callback); - }); } }); @@ -298,18 +293,10 @@ DB.prototype.saveMetadata = function(callback) { callback = callback || defaultCallback; - var threshold = self.lastSavedMetadataThreshold; - if (self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + threshold) { - return callback(); - } - var metadata = { - tip: self.tip ? self.tip.hash : null, - tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, + tip: self.tip ? self.tip.hash : null }; - self.lastSavedMetadata = new Date(); - this.store.put('metadata', JSON.stringify(metadata), {}, callback); }; @@ -416,7 +403,7 @@ DB.prototype.findCommonAncestor = function(block, done) { var mainHashesMap = {}; var forkHashesMap = {}; - mainHahesMap[mainPosition] = true; + mainHashesMap[mainPosition] = true; forkHashesMap[forkPosition] = true; var commonAncestor = null; @@ -426,8 +413,9 @@ DB.prototype.findCommonAncestor = function(block, done) { return !commonAncestor; }, function(next) { + if(mainPosition) { - var mainBlockIndex = self.node.services.bitcoind.getBlockIndex(mainTip); + var mainBlockIndex = self.node.services.bitcoind.getBlockIndex(mainPosition); if(mainBlockIndex && mainBlockIndex.prevHash) { mainHashesMap[mainBlockIndex.prevHash] = true; mainPosition = mainBlockIndex.prevHash; @@ -437,7 +425,7 @@ DB.prototype.findCommonAncestor = function(block, done) { } if(forkPosition) { - var forkBlockIndex = self.node.services.bitcoind.getBlockIndex(forkTip); + var forkBlockIndex = self.node.services.bitcoind.getBlockIndex(forkPosition); if(forkBlockIndex && forkBlockIndex.prevHash) { forkHashesMap[forkBlockIndex.prevHash] = true; forkPosition = forkBlockIndex.prevHash; @@ -480,6 +468,7 @@ DB.prototype.syncRewind = function(block, done) { if (err) { return done(err); } + log.warn('Reorg common ancestor found:', ancestorHash); // Rewind the chain to the common ancestor async.whilst( function() { @@ -537,7 +526,6 @@ DB.prototype.sync = function() { } self.bitcoindSyncing = true; - self.lastSavedMetadataThreshold = 30000; var height; @@ -601,7 +589,6 @@ DB.prototype.sync = function() { } self.bitcoindSyncing = false; - self.lastSavedMetadataThreshold = 0; self.saveMetadata(); // If bitcoind is completely synced diff --git a/test/services/db.unit.js b/test/services/db.unit.js index 22106e5f..a2293df4 100644 --- a/test/services/db.unit.js +++ b/test/services/db.unit.js @@ -162,19 +162,20 @@ describe('DB Service', function() { }); it('metadata from the database if it exists', function(done) { + var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; var node = { network: Networks.testnet, datadir: 'testdir', services: { bitcoind: { genesisBuffer: genesisBuffer, + getBlockIndex: sinon.stub().returns({tip:tipHash}), on: sinon.stub() } } }; var tip = Block.fromBuffer(genesisBuffer); var db = new TestDB({node: node}); - var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; db.getMetadata = sinon.stub().callsArgWith(0, null, { tip: tipHash, tipHeight: 0 @@ -535,12 +536,7 @@ describe('DB Service', function() { put: function(key, value, options, callback) { key.should.equal('metadata'); JSON.parse(value).should.deep.equal({ - tip: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', - tipHeight: 0, - cache: { - hashes: {}, - chainHashes: {} - } + tip: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' }); options.should.deep.equal({}); callback.should.be.a('function'); @@ -549,22 +545,6 @@ describe('DB Service', function() { }; db.saveMetadata(); }); - it('will not call store with threshold', function(done) { - var db = new DB(baseConfig); - db.lastSavedMetadata = new Date(); - db.lastSavedMetadataThreshold = 30000; - var put = sinon.stub(); - db.store = { - put: put - }; - db.saveMetadata(function(err) { - if (err) { - throw err; - } - put.callCount.should.equal(0); - done(); - }); - }); }); describe('#getMetadata', function() { @@ -689,88 +669,79 @@ describe('DB Service', function() { }); }); - describe('#getHashes', function() { - - it('should get an array of chain hashes', function(done) { - - var blocks = {}; - var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); - var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); - var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); - blocks[genesisBlock.hash] = genesisBlock; - blocks[block1.hash] = block1; - blocks[block2.hash] = block2; - - var db = new DB(baseConfig); - db.genesis = genesisBlock; - db.getPrevHash = function(blockHash, cb) { - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex'); - cb(null, prevHash); - }; - - db.tip = block2; - - // the test - db.getHashes(block2.hash, function(err, hashes) { - should.not.exist(err); - should.exist(hashes); - hashes.length.should.equal(3); - done(); - }); - - }); - }); - describe('#findCommonAncestor', function() { - it('will find an ancestor 6 deep', function() { + it('will find an ancestor 6 deep', function(done) { var db = new DB(baseConfig); - db.getHashes = function(tipHash, callback) { - callback(null, chainHashes); - }; db.tip = { - hash: chainHashes[chainHashes.length] + hash: chainHashes[chainHashes.length - 1] }; + var expectedAncestor = chainHashes[chainHashes.length - 6]; + var mainBlocks = {}; + for(var i = chainHashes.length - 1; i > chainHashes.length - 10; i--) { + var hash = chainHashes[i]; + var prevHash = hexlebuf(chainHashes[i - 1]); + mainBlocks[hash] = { + header: { + prevHash: prevHash + } + }; + } + var forkedBlocks = { 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { header: { prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') - } + }, + hash: 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82' }, '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { header: { prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') - } + }, + hash: '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a' }, 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { header: { prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') - } + }, + hash: 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c' }, '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { header: { prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') - } + }, + hash: '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31' }, 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { header: { prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') - } + }, + hash: 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453' }, '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { header: { prevHash: hexlebuf(expectedAncestor) - } - }, + }, + hash: '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618' + } }; db.node.services = {}; db.node.services.bitcoind = { getBlockIndex: function(hash) { - var block = forkedBlocks[hash]; + var forkedBlock = forkedBlocks[hash]; + var mainBlock = mainBlocks[hash]; + var prevHash; + if (forkedBlock && forkedBlock.header.prevHash) { + prevHash = BufferUtil.reverse(forkedBlock.header.prevHash).toString('hex'); + } else if (mainBlock && mainBlock.header.prevHash){ + prevHash = BufferUtil.reverse(mainBlock.header.prevHash).toString('hex'); + } else { + return null; + } return { - prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex') + prevHash: prevHash }; } }; @@ -780,6 +751,7 @@ describe('DB Service', function() { throw err; } ancestorHash.should.equal(expectedAncestor); + done(); }); }); }); @@ -854,7 +826,6 @@ describe('DB Service', function() { __height: 0, hash: lebufhex(block.header.prevHash) }; - db.getHashes = sinon.stub().callsArgWith(1, null); db.saveMetadata = sinon.stub(); db.emit = sinon.stub(); db.cache = {