diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 6d1b2b43..a531ef23 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -753,10 +753,13 @@ Chain.prototype.resetHeight = function resetHeight(height) { for (i = height + 1; i < count; i++) { existing = this.db.get(i); assert(existing); + // this.db.remove(i); + this.db.drop(i); delete this.heightLookup[existing.hash]; - this.db.remove(i); } + this.db.truncate(height); + // Reset the orphan map completely. There may // have been some orphans on a forked chain we // no longer need. @@ -772,6 +775,63 @@ Chain.prototype.resetHeight = function resetHeight(height) { this.emit('tip', this.tip); }; +Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { + var self = this; + var count = this.db.count(); + var i, lock; + + assert(height <= count - 1); + assert(this.tip); + + if (height === count - 1) + return utils.nextTick(callback); + + lock = this.locked; + this.locked = true; + + i = height + 1; + + function next() { + if (i === count) + return self.db.truncateAsync(height, done); + + self.db.getAsync(i, function(err, existing) { + if (err) + return done(err); + + assert(existing); + + delete self.heightLookup[existing.hash]; + self.db.drop(i); + i++; + next(); + }); + } + + function done(err) { + self.locked = lock; + + if (err) + return callback(err); + + // Reset the orphan map completely. There may + // have been some orphans on a forked chain we + // no longer need. + self.emit('purge', self.orphan.count, self.orphan.size); + self.orphan.map = {}; + self.orphan.bmap = {}; + self.orphan.count = 0; + self.orphan.size = 0; + + self.tip = self.db.get(height); + assert(self.tip); + self.height = self.tip.height; + self.emit('tip', self.tip); + + return callback(); + } +}; + Chain.prototype.revertHeight = function revertHeight(height, callback) { var self = this; var chainHeight; @@ -1399,6 +1459,94 @@ Chain.prototype.getLocator = function getLocator(start) { return hashes; }; +Chain.prototype.getLocatorAsync = function getLocatorAsync(start, callback) { + var self = this; + var hashes = []; + var top = this.height; + var step = 1; + var i, called; + + if (start) { + if (utils.isBuffer(start)) + start = utils.toHex(start); + else if (start.hash) + start = start.hash('hex'); + } + + if (typeof start === 'string') { + top = this.heightLookup[start]; + if (top == null) { + // 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; + } + } else if (typeof start === 'number') { + top = start; + } + + function done(err) { + if (called) + return; + + called = true; + + if (err) + return callback(err); + + return callback(null, hashes); + } + + this.db.hasAsync(top, function(err, has) { + var pending; + + if (err) + return done(err); + + 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; + } + + pending = hashes.length; + + hashes.forEach(function(height, i) { + if (typeof height === 'string') { + if (!--pending) + done(); + return; + } + + self.db.getAsync(height, function(err, existing) { + if (err) + return done(err); + + if (!existing) + return done(new Error('Potential reset.')); + + hashes[i] = existing.hash; + + if (!--pending) + done(); + }); + }); + }); +}; + Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { var self = this; var root; diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 2f88f327..de227a15 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -323,7 +323,7 @@ ChainDB.prototype.saveAsync = function saveAsync(entry, callback) { }); }; -ChainDB.prototype.remove = function remove(height) { +ChainDB.prototype.drop = function drop(height) { assert(height >= 0); // Potential race condition here. Not sure how @@ -333,8 +333,17 @@ ChainDB.prototype.remove = function remove(height) { delete this._queue[height]; } - this._writeSync(this._nullBlock, height * BLOCK_SIZE); delete this._cache[height]; +}; + +ChainDB.prototype.remove = function remove(height) { + assert(height >= 0); + + // Drop the queue and cache + this.drop(height); + + // Write a null block + this._writeSync(this._nullBlock, height * BLOCK_SIZE); // If we deleted several blocks at the end, go back // to the last non-null block and truncate the file @@ -345,13 +354,13 @@ ChainDB.prototype.remove = function remove(height) { assert(height >= 0); - this.truncateSync(height); + this.truncate(height); } return true; }; -ChainDB.prototype.truncateSync = function truncateSync(height) { +ChainDB.prototype.truncate = function truncate(height) { var size = (height + 1) * BLOCK_SIZE; if (!bcoin.fs) { @@ -417,6 +426,31 @@ ChainDB.prototype.has = function has(height) { return utils.read32(data, 0) !== 0; }; +ChainDB.prototype.hasAsync = function hasAsync(height, callback) { + var data; + + callback = utils.asyncify(callback); + + if (this._queue[height] || this._cache[height]) + return callback(null, true); + + if (height < 0 || height == null) + return callback(null, false); + + if ((height + 1) * BLOCK_SIZE > this.size) + return callback(null, false); + + this._readAsync(4, height * BLOCK_SIZE, function(err, data) { + if (err) + return callback(err); + + if (!data) + return callback(null, false); + + return callback(null, utils.read32(data, 0) !== 0); + }); +}; + ChainDB.prototype._readSync = function _readSync(size, offset) { var index = 0; var data, bytes;