diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index b030ae12..448358f3 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -37,10 +37,11 @@ function Chain(options) { this.db = new bcoin.chaindb(this); this.request = new utils.RequestCache(); this.loading = false; - this.tip = null; - this.height = -1; this.mempool = options.mempool; this.blockdb = options.blockdb; + this.resetting = false; + this.reverting = false; + this.syncing = false; this.locked = false; this.pending = []; this.pendingBlocks = {}; @@ -144,11 +145,6 @@ Chain.prototype._init = function _init() { self.mempool.removeBlock(block); }); - this.db.on('tip', function(tip) { - self.tip = tip; - self.height = tip.height; - }); - this.loading = true; utils.debug('Chain is loading.'); @@ -179,22 +175,19 @@ Chain.prototype._init = function _init() { }); }; +Chain.prototype.__defineGetter__('tip', function() { + return this.db.tip; +}); + +Chain.prototype.__defineGetter__('height', function() { + return this.db.height; +}); + Chain.prototype._ensureGenesis = function _ensureGenesis(callback) { var self = this; callback = utils.asyncify(callback); - this.db.save(bcoin.chainblock.fromJSON(this, { - hash: network.genesis.hash, - version: network.genesis.version, - prevBlock: network.genesis.prevBlock, - merkleRoot: network.genesis.merkleRoot, - ts: network.genesis.ts, - bits: network.genesis.bits, - nonce: network.genesis.nonce, - height: 0 - })); - if (!this.blockdb) return callback(); @@ -681,6 +674,7 @@ Chain.prototype.resetHeight = function resetHeight(height) { var count = this.db.count(); var i, existing; + assert(!this.resetting); assert(height <= count - 1); assert(this.tip); @@ -704,17 +698,17 @@ Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { var count = this.db.count(); var i, lock; + assert(!this.resetting); assert(height <= count - 1); assert(this.tip); if (height === count - 1) return utils.nextTick(callback); - lock = this.locked; - this.locked = true; + this.resetting = true; this.db.resetHeightAsync(height, function(err) { - self.locked = lock; + self.resetting = false; if (err) return callback(err); @@ -735,16 +729,15 @@ Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { Chain.prototype.revertHeight = function revertHeight(height, callback) { var self = this; var chainHeight; - var lock = this.locked; - assert(!this.locked); + assert(!this.reverting); callback = utils.asyncify(callback); - this.locked = true; + this.reverting = true; function done(err, result) { - self.locked = lock; + self.reverting = false; callback(err, result); } @@ -756,8 +749,7 @@ Chain.prototype.revertHeight = function revertHeight(height, callback) { if (!this.blockdb) { if (height > chainHeight) return done(new Error('Cannot reset height.')); - this.resetHeight(height); - return done(); + return this.resetHeightAsync(height, done); } this.blockdb.getHeight(function(err, blockHeight) { @@ -780,9 +772,7 @@ Chain.prototype.revertHeight = function revertHeight(height, callback) { if (err) return done(err); - self.resetHeight(height); - - return done(); + self.resetHeightAsync(height, done); }, function(block) { self.emit('remove block', block); }); @@ -791,32 +781,34 @@ Chain.prototype.revertHeight = function revertHeight(height, callback) { Chain.prototype._revertLast = function _revertLast(existing, callback) { var self = this; - return this._removeBlock(existing.hash, function(err, existingBlock) { + this._removeBlock(existing.hash, function(err, existingBlock) { if (err) return callback(err); - self.resetHeight(existing.height - 1); + self.resetHeightAsync(existing.height - 1, function(err) { + if (err) + return callback(err); - if (existingBlock) - self.emit('remove block', existingBlock); + if (existingBlock) + self.emit('remove block', existingBlock); - return callback(); + return callback(); + }); }); }; Chain.prototype.syncHeight = function syncHeight(callback) { var self = this; var chainHeight; - var lock = this.locked; callback = utils.asyncify(callback); - assert(!this.locked); + assert(!this.syncing); - this.locked = true; + this.syncing = true; function done(err, result) { - self.locked = lock; + self.syncing = false; callback(err, result); } @@ -842,8 +834,7 @@ Chain.prototype.syncHeight = function syncHeight(callback) { if (blockHeight < chainHeight) { utils.debug('BlockDB is higher than ChainDB. Syncing...'); - self.resetHeight(blockHeight); - return done(); + return self.resetHeightAsync(blockHeight, done); } if (blockHeight > chainHeight) { @@ -874,6 +865,9 @@ 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; @@ -890,7 +884,7 @@ Chain.prototype.add = function add(initial, peer, callback) { (function next(block) { var hash = block.hash('hex'); var prevHash = block.prevBlock; - var prevHeight, entry, existing, checkpoint, prev, orphan; + var prevHeight, entry, checkpoint, prev, orphan; // Special case for genesis block. if (block.isGenesis()) @@ -1018,52 +1012,54 @@ Chain.prototype.add = function add(initial, peer, callback) { } // See if the entry already exists. - existing = self.db.get(entry.height); - - // Entry already exists. - if (existing) { - // We already have this block. Do regular - // orphan resolution (won't do anything). - // NOTE: Wrap this in a nextTick to avoid - // a stack overflow if there are a lot of - // existing blocks. - if (existing.hash === hash) { - self.emit('exists', block, { - height: entry.height, - hash: entry.hash - }, peer); - return utils.nextTick(handleOrphans); - } - - // 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. - assert(self.db.getHeight(entry.hash) == null); - - // 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) - return done(); - - // 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) { + if (self.db.has(entry.height)) { + return self.db.getAsync(entry.height, function(err, existing) { if (err) return done(err); - self.emit('fork', block, { - height: existing.height, - expected: existing.hash, - received: entry.hash, - checkpoint: false - }, peer); + // We already have this block. Do regular + // orphan resolution (won't do anything). + // NOTE: Wrap this in a nextTick to avoid + // a stack overflow if there are a lot of + // existing blocks. + if (existing.hash === hash) { + self.emit('exists', block, { + height: entry.height, + hash: entry.hash + }, peer); + return utils.nextTick(handleOrphans); + } - return done(); + // 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. + assert(self.db.getHeight(entry.hash) == null); + + // 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) + return done(); + + // 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) { + if (err) + return done(err); + + self.emit('fork', block, { + height: existing.height, + expected: existing.hash, + received: entry.hash, + checkpoint: false + }, peer); + + return done(); + }); }); } @@ -1071,6 +1067,8 @@ Chain.prototype.add = function add(initial, peer, callback) { assert(self.db.getHeight(entry.hash) == null); // Lookup previous entry. + // We can do this synchronously: + // it is already cached. prev = self.db.get(prevHeight); assert(prev); @@ -1247,7 +1245,7 @@ Chain.prototype.byTime = function byTime(ts) { }; Chain.prototype.hasBlock = function hasBlock(hash) { - return !!this.byHash(hash); + return this.db.has(hash); }; Chain.prototype.hasOrphan = function hasOrphan(hash) { @@ -1400,49 +1398,47 @@ Chain.prototype.getLocatorAsync = function getLocatorAsync(start, callback) { return callback(null, hashes); } - this.db.hasAsync(top, function(err, has) { - var pending; + try { + assert(this.db.has(top)); + } catch (e) { + return done(new Error('Potential reset.')); + } - if (err) - return done(err); + var pending; - if (!has) - return done(new Error('Potential reset.')); + i = top; + for (;;) { + hashes.push(i); + i = i - step; + if (i <= 0) { + if (i + step !== 0) + hashes.push(0); + break; + } + if (hashes.length >= 10) + step *= 2; + } - i = top; - for (;;) { - hashes.push(i); - i = i - step; - if (i <= 0) { - if (i + step !== 0) - hashes.push(0); - break; - } - if (hashes.length >= 10) - step *= 2; + pending = hashes.length; + + hashes.forEach(function(height, i) { + if (typeof height === 'string') { + if (!--pending) + done(); + return; } - pending = hashes.length; + self.db.getAsync(height, function(err, existing) { + if (err) + return done(err); - hashes.forEach(function(height, i) { - if (typeof height === 'string') { - if (!--pending) - done(); - return; - } + if (!existing) + return done(new Error('Potential reset.')); - self.db.getAsync(height, function(err, existing) { - if (err) - return done(err); + hashes[i] = existing.hash; - if (!existing) - return done(new Error('Potential reset.')); - - hashes[i] = existing.hash; - - if (!--pending) - done(); - }); + if (!--pending) + done(); }); }); }; @@ -1467,11 +1463,11 @@ Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { }; Chain.prototype.getHeight = function getHeight(hash) { - var entry = this.byHash(hash); - if (!entry) + var height = this.db.getHeight(hash); + if (height == null) return -1; - return entry.height; + return height; }; Chain.prototype.getNextBlock = function getNextBlock(hash) { diff --git a/lib/bcoin/chainblock.js b/lib/bcoin/chainblock.js index a6c67935..657275f2 100644 --- a/lib/bcoin/chainblock.js +++ b/lib/bcoin/chainblock.js @@ -53,6 +53,8 @@ ChainBlock.prototype.getProof = function getProof() { }; ChainBlock.prototype.getChainwork = function getChainwork() { + if (!this.chain.db) + return; var prev = this.prev; return (prev ? prev.chainwork : new bn(0)).add(this.getProof()); }; diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 3585ccfd..7ddcc111 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -61,9 +61,21 @@ function ChainDB(chain, options) { inherits(ChainDB, EventEmitter); ChainDB.prototype._init = function _init() { + var genesis = bcoin.chainblock.fromJSON(this.chain, { + hash: network.genesis.hash, + version: network.genesis.version, + prevBlock: network.genesis.prevBlock, + merkleRoot: network.genesis.merkleRoot, + ts: network.genesis.ts, + bits: network.genesis.bits, + nonce: network.genesis.nonce, + height: 0 + }); + if (!bcoin.fs) { utils.debug('`fs` module not available. Falling back to ramdisk.'); this.ramdisk = new bcoin.ramdisk(40 * 1024 * 1024); + this.saveSync(genesis); return; } @@ -88,6 +100,13 @@ ChainDB.prototype._init = function _init() { } this.fd = fs.openSync(this.file, 'r+'); + + if (this.size === 0) { + this.saveSync(genesis); + } else { + this.getSync(0); + assert(this.tip.hash === genesis.hash); + } }; ChainDB.prototype.load = function load(start, callback) { @@ -403,15 +422,19 @@ ChainDB.prototype.drop = function drop(height) { }; ChainDB.prototype.resetHeight = function resetHeight(height) { + var size, count, existing; + if (typeof height === 'string') height = this.heightLookup[height]; assert(height >= 0); - var size = (height + 1) * BLOCK_SIZE; - var count = this.count(); + size = (height + 1) * BLOCK_SIZE; + count = this.count(); + if (height === count - 1) return; + assert(height <= count - 1); assert(this.tip); @@ -422,19 +445,10 @@ ChainDB.prototype.resetHeight = function resetHeight(height) { delete this.heightLookup[existing.hash]; } - if (!bcoin.fs) { + if (!bcoin.fs) this.ramdisk.truncate(size); - - this.size = size; - this.highest = height; - this.tip = this.get(height); - assert(this.tip); - this.height = this.tip.height; - this.emit('tip', this.tip); - return; - } - - fs.ftruncateSync(this.fd, size); + else + fs.ftruncateSync(this.fd, size); this.size = size; this.highest = height; @@ -446,7 +460,7 @@ ChainDB.prototype.resetHeight = function resetHeight(height) { ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { var self = this; - var called; + var size, count, pending, called; if (typeof height === 'string') height = this.heightLookup[height]; @@ -455,15 +469,16 @@ ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback) callback = utils.asyncify(callback); - var size = (height + 1) * BLOCK_SIZE; - var count = this.count() - 1; + size = (height + 1) * BLOCK_SIZE; + count = this.count() - 1; + pending = count - (height + 1); + if (height === count - 1) return callback(); + assert(height <= count - 1); assert(this.tip); - var pending = count - (height + 1); - for (i = height + 1; i < count; i++) dropEntry(i); @@ -491,12 +506,6 @@ ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback) if (!bcoin.fs) { self.ramdisk.truncate(size); - self.size = size; - self.highest = height; - self.tip = self.get(height); - assert(self.tip); - self.height = self.tip.height; - self.emit('tip', self.tip); return callback(); } @@ -583,6 +592,9 @@ ChainDB.prototype._readAsync = function _readAsync(size, offset, callback) { return callback(err); } + if (!bytes) + throw new Error('_readAsync() failed.'); + index += bytes; size -= bytes; offset += bytes; @@ -655,6 +667,9 @@ ChainDB.prototype._writeAsync = function _writeAsync(data, offset, callback) { return callback(err); } + if (!bytes) + throw new Error('_writeAsync() failed.'); + index += bytes; size -= bytes; offset += bytes;