diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index 205abc56..63487acb 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -113,48 +113,39 @@ ChainDB.layout = layout; */ ChainDB.prototype._open = co(function* open() { - var state, options, block, entry; + var state; - this.logger.info('Starting chain load.'); + this.logger.info('Opening ChainDB...'); yield this.db.open(); - yield this.db.checkVersion('V', 2); state = yield this.getState(); - options = yield this.getOptions(); - - if (options) { - // Verify the options haven't changed. - this.options.verify(options); - if (this.options.forceWitness) - yield this.saveOptions(); - } else { - yield this.saveOptions(); - } - - // Verify deployment params have not changed. - yield this.verifyDeployments(); if (state) { - // Grab the chainstate if we have one. - this.state = state; + // Verify options have not changed. + yield this.verifyOptions(); + + // Verify deployment params have not changed. + yield this.verifyDeployments(); // Load state caches. this.stateCache = yield this.getStateCache(); + + // Grab the chainstate if we have one. + this.state = state; + + this.logger.info('ChainDB successfully loaded.'); } else { - // Otherwise write the genesis block. - // (We assume this database is fresh). - block = Block.fromRaw(this.network.genesisBlock, 'hex'); - block.setHeight(0); - entry = ChainEntry.fromBlock(this.chain, block); + // Database is fresh. + // Write initial state. + yield this.saveOptions(); + yield this.saveDeployments(); + yield this.saveGenesis(); - this.logger.info('Writing genesis block to ChainDB.'); - yield this.save(entry, block, new CoinView()); + this.logger.info('ChainDB successfully initialized.'); } - this.logger.info('Chain successfully loaded.'); - this.logger.info( 'Chain State: hash=%s tx=%d coin=%d value=%s.', this.state.rhash, @@ -526,6 +517,25 @@ ChainDB.prototype.getState = co(function* getState() { return ChainState.fromRaw(data); }); +/** + * Write genesis block to database. + * @returns {Promise} + */ + +ChainDB.prototype.saveGenesis = co(function* saveGenesis() { + var genesis = this.network.genesisBlock; + var block = Block.fromRaw(genesis, 'hex'); + var entry; + + block.setHeight(0); + + entry = ChainEntry.fromBlock(this.chain, block); + + this.logger.info('Writing genesis block to ChainDB.'); + + yield this.save(entry, block, new CoinView()); +}); + /** * Retrieve the tip entry from the tip record. * @returns {Promise} - Returns {@link ChainOptions}. @@ -540,6 +550,165 @@ ChainDB.prototype.getOptions = co(function* getOptions() { return ChainOptions.fromRaw(data); }); +/** + * Verify current options against db options. + * @returns {Promise} + */ + +ChainDB.prototype.verifyOptions = co(function* verifyOptions() { + var options = yield this.getOptions(); + + assert(options, 'No options found.'); + + this.options.verify(options); + + if (this.options.forceWitness) + yield this.saveOptions(); +}); + +/** + * Get state caches. + * @returns {Promise} - Returns {@link StateCache}. + */ + +ChainDB.prototype.getStateCache = co(function* getStateCache() { + var stateCache = new StateCache(this.network); + var i, items, item, key, bit, hash, state; + + items = yield this.db.range({ + gte: layout.v(0, constants.ZERO_HASH), + lte: layout.v(255, constants.MAX_HASH), + values: true + }); + + for (i = 0; i < items.length; i++) { + item = items[i]; + key = layout.vv(item.key); + bit = key[0]; + hash = key[1]; + state = item.value[0]; + stateCache.insert(bit, hash, state); + } + + return stateCache; +}); + +/** + * Save deployment table. + * @returns {Promise} + */ + +ChainDB.prototype.saveDeployments = function saveDeployments() { + var batch = this.db.batch(); + this.writeDeployments(batch); + return batch.write(); +}; + +/** + * Save deployment table. + * @returns {Promise} + */ + +ChainDB.prototype.writeDeployments = function writeDeployments(batch) { + var bw = new BufferWriter(); + var i, deployment; + + bw.writeU8(this.network.deploys.length); + + for (i = 0; i < this.network.deploys.length; i++) { + deployment = this.network.deploys[i]; + bw.writeU8(deployment.bit); + bw.writeU32(deployment.startTime); + bw.writeU32(deployment.timeout); + } + + batch.put(layout.V, bw.render()); +}; + +/** + * Check for outdated deployments. + * @private + * @returns {Promise} + */ + +ChainDB.prototype.checkDeployments = co(function* checkDeployments() { + var raw = yield this.db.get(layout.V); + var invalid = []; + var i, br, count, deployment; + var bit, start, timeout; + + assert(raw, 'No deployment table found.'); + + br = new BufferReader(raw); + + count = br.readU8(); + + for (i = 0; i < count; i++) { + bit = br.readU8(); + start = br.readU32(); + timeout = br.readU32(); + deployment = this.network.byBit(bit); + + if (deployment + && start === deployment.startTime + && timeout === deployment.timeout) { + continue; + } + + invalid.push(bit); + } + + return invalid; +}); + +/** + * Potentially invalidate state cache. + * @returns {Promise} + */ + +ChainDB.prototype.verifyDeployments = co(function* verifyDeployments() { + var invalid = yield this.checkDeployments(); + var i, bit, batch; + + if (invalid.length === 0) + return true; + + batch = this.db.batch(); + + for (i = 0; i < invalid.length; i++) { + bit = invalid[i]; + this.logger.warning('Versionbit deployment params modified.'); + this.logger.warning('Invalidating cache for bit %d.', bit); + yield this.invalidateCache(bit, batch); + } + + this.writeDeployments(batch); + + yield batch.write(); + + return false; +}); + +/** + * Invalidate state cache. + * @private + * @returns {Promise} + */ + +ChainDB.prototype.invalidateCache = co(function* invalidateCache(bit, batch) { + var i, keys, key; + + keys = yield this.db.keys({ + gte: layout.v(bit, constants.ZERO_HASH), + lte: layout.v(bit, constants.MAX_HASH) + }); + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + batch.del(key); + } +}); + /** * Get the _next_ block hash (does not work by height). * @param {Hash} hash @@ -740,6 +909,7 @@ ChainDB.prototype.getUndoView = co(function* getUndoView(block) { for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; input.coin = undo.apply(index++, view, input.prevout); + assert(input.coin); } } @@ -811,86 +981,6 @@ ChainDB.prototype.getFullBlock = co(function* getFullBlock(hash) { return block; }); -/** - * Get state caches. - * @returns {Promise} - Returns {@link StateCache}. - */ - -ChainDB.prototype.getStateCache = co(function* getStateCache() { - var stateCache = new StateCache(this.network); - var i, items, item; - - items = yield this.db.range({ - gte: layout.v(0, constants.ZERO_HASH), - lte: layout.v(255, constants.MAX_HASH), - values: true - }); - - for (i = 0; i < items.length; i++) { - item = items[i]; - stateCache.setRaw(item.key, item.value); - } - - return stateCache; -}); - -/** - * Invalidate state cache. - * @private - * @returns {Promise} - */ - -ChainDB.prototype.invalidateCache = co(function* invalidateCache(bit, batch) { - var i, keys, key; - - keys = yield this.db.keys({ - gte: layout.v(bit, constants.ZERO_HASH), - lte: layout.v(bit, constants.MAX_HASH) - }); - - for (i = 0; i < keys.length; i++) { - key = keys[i]; - batch.del(key); - } -}); - -/** - * Potentially invalidate state cache. - * @returns {Promise} - */ - -ChainDB.prototype.verifyDeployments = co(function* verifyDeployments() { - var expected = this.stateCache.toDeployments(); - var current = yield this.db.get(layout.V); - var i, invalid, bit, batch; - - if (!current) { - this.logger.info('Writing deployment params to ChainDB.'); - yield this.db.put(layout.V, expected); - return true; - } - - invalid = this.stateCache.verifyDeployments(current); - - if (invalid.length === 0) - return true; - - batch = this.db.batch(); - - for (i = 0; i < invalid.length; i++) { - bit = invalid[i]; - this.logger.warning('Versionbit deployment params modified.'); - this.logger.warning('Invalidating cache for bit %d.', bit); - yield this.invalidateCache(bit, batch); - } - - batch.put(layout.V, expected); - - yield batch.write(); - - return false; -}); - /** * Fill a transaction with coins (only unspents). * @param {TX} tx @@ -1740,6 +1830,9 @@ ChainDB.prototype.disconnectBlock = co(function* disconnectBlock(block) { } } + // Remove undo coins. + this.del(layout.u(block.hash())); + // Commit new coin state. view = view.toArray(); @@ -1754,8 +1847,6 @@ ChainDB.prototype.disconnectBlock = co(function* disconnectBlock(block) { this.coinCache.push(coins.hash, raw); } } - - this.del(layout.u(block.hash())); }); /** @@ -2026,7 +2117,7 @@ ChainState.fromRaw = function fromRaw(data) { function StateCache(network) { this.network = network; - this.cache = []; + this.bits = []; this.updates = []; this._init(); } @@ -2035,59 +2126,17 @@ StateCache.prototype._init = function _init() { var i, deployment; for (i = 0; i < 32; i++) - this.cache.push(null); + this.bits.push(null); for (i = 0; i < this.network.deploys.length; i++) { deployment = this.network.deploys[i]; - assert(!this.cache[deployment.bit]); - this.cache[deployment.bit] = {}; + assert(!this.bits[deployment.bit]); + this.bits[deployment.bit] = {}; } }; -StateCache.prototype.toDeployments = function toDeployments() { - var bw = new BufferWriter(); - var i, deployment; - - bw.writeU8(this.network.deploys.length); - - for (i = 0; i < this.network.deploys.length; i++) { - deployment = this.network.deploys[i]; - bw.writeU8(deployment.bit); - bw.writeU32(deployment.startTime); - bw.writeU32(deployment.timeout); - } - - return bw.render(); -}; - -StateCache.prototype.verifyDeployments = function verifyDeployments(raw) { - var br = new BufferReader(raw); - var invalid = []; - var i, count, deployment; - var bit, start, timeout; - - count = br.readU8(); - - for (i = 0; i < count; i++) { - bit = br.readU8(); - start = br.readU32(); - timeout = br.readU32(); - deployment = this.network.byBit(bit); - - if (deployment - && start === deployment.startTime - && timeout === deployment.timeout) { - continue; - } - - invalid.push(bit); - } - - return invalid; -}; - StateCache.prototype.set = function set(bit, entry, state) { - var cache = this.cache[bit]; + var cache = this.bits[bit]; assert(cache); @@ -2098,7 +2147,7 @@ StateCache.prototype.set = function set(bit, entry, state) { }; StateCache.prototype.get = function get(bit, entry) { - var cache = this.cache[bit]; + var cache = this.bits[bit]; var state; assert(cache); @@ -2120,7 +2169,7 @@ StateCache.prototype.drop = function drop() { for (i = 0; i < this.updates.length; i++) { update = this.updates[i]; - cache = this.cache[update.bit]; + cache = this.bits[update.bit]; assert(cache); delete cache[update.hash]; } @@ -2128,12 +2177,8 @@ StateCache.prototype.drop = function drop() { this.updates.length = 0; }; -StateCache.prototype.setRaw = function setRaw(key, value) { - var pair = layout.vv(key); - var bit = pair[0]; - var hash = pair[1]; - var state = value[0]; - var cache = this.cache[bit]; +StateCache.prototype.insert = function insert(bit, hash, state) { + var cache = this.bits[bit]; assert(cache); cache[hash] = state; }; diff --git a/migrate/chaindb1to2.js b/migrate/chaindb1to2.js index 0a04fa9c..356bb5e5 100644 --- a/migrate/chaindb1to2.js +++ b/migrate/chaindb1to2.js @@ -65,23 +65,23 @@ var updateVersion = co(function* updateVersion() { }); var checkTipIndex = co(function* checkTipIndex() { - var iter, item; - - iter = db.iterator({ + var keys = yield db.keys({ gte: pair('p', constants.ZERO_HASH), lte: pair('p', constants.MAX_HASH) }); - item = yield iter.next(); - - if (!item) { + if (keys.length === 0) { console.log('No tip index found.'); console.log('Please run migrate/ensure-tip-index.js first!'); process.exit(1); return; } - yield iter.end(); + if (keys.length < 3) { + console.log('Note: please run ensure-tip-index.js if you haven\'t yet.'); + yield co.timeout(2000); + return; + } }); var updateOptions = co(function* updateOptions() { diff --git a/migrate/ensure-tip-index.js b/migrate/ensure-tip-index.js index 5bcdc96e..dfa78f5a 100644 --- a/migrate/ensure-tip-index.js +++ b/migrate/ensure-tip-index.js @@ -88,18 +88,6 @@ var isMainChain = co(function* isMainChain(entry, tip) { return false; }); -var removeOld = co(function* removeOld() { - var i, keys, key; - - keys = yield db.keys({ - gte: pair('a', constants.ZERO_HASH), - lte: pair('a', constants.MAX_HASH) - }); - - for (i = 0; i < keys.length; i++) - batch.del(keys[i]); -}); - // And this insane function is why we should // be indexing tips in the first place! var indexTips = co(function* indexTips() { @@ -163,7 +151,6 @@ co.spawn(function* () { console.log('Opened %s.', file); batch = db.batch(); yield checkVersion(); - yield removeOld(); yield indexTips(); yield batch.write(); }).then(function() {