diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index c5455c8d..17c189fc 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -246,6 +246,8 @@ function ChainDB(chain, options) { this.keepBlocks = options.keepBlocks || 288; this.prune = !!options.prune; this.state = new ChainState(); + this.pending = null; + this.current = null; this.loaded = false; @@ -328,6 +330,85 @@ ChainDB.prototype._close = function close(callback) { this.db.close(callback); }; +/** + * Start a batch. + * @returns {Batch} + */ + +ChainDB.prototype.start = function start() { + assert(!this.current); + assert(!this.pending); + this.current = this.db.batch(); + this.pending = this.state.clone(); + return this.current; +}; + +/** + * Put key and value to current batch. + * @param {String} key + * @param {Buffer} value + */ + +ChainDB.prototype.put = function put(key, value) { + assert(this.current); + this.current.put(key, value); +}; + +/** + * Delete key from current batch. + * @param {String} key + */ + +ChainDB.prototype.del = function del(key) { + assert(this.current); + this.current.del(key); +}; + +/** + * Get current batch. + * @returns {Batch} + */ + +ChainDB.prototype.batch = function batch() { + assert(this.current); + return this.current; +}; + +/** + * Drop current batch. + * @returns {Batch} + */ + +ChainDB.prototype.drop = function drop() { + assert(this.current); + assert(this.pending); + this.current.clear(); + this.current = null; + this.pending = null; +}; + +/** + * Commit current batch. + * @param {Function} callback + */ + +ChainDB.prototype.commit = function commit(callback) { + var self = this; + assert(this.current); + assert(this.pending); + this.current.write(function(err) { + if (err) { + self.current = null; + self.pending = null; + return callback(err); + } + self.current = null; + self.state = self.pending; + self.pending = null; + callback(); + }); +}; + /** * Add an entry to the LRU cache. * @param {ChainEntry} entry @@ -575,35 +656,41 @@ ChainDB.prototype.get = function get(hash, callback) { */ ChainDB.prototype.save = function save(entry, block, view, connect, callback) { - var batch = this.db.batch(); + var self = this; var hash = block.hash(); var height = new Buffer(4); + this.start(); + height.writeUInt32LE(entry.height, 0, true); - batch.put(layout.h(hash), height); - batch.put(layout.e(hash), entry.toRaw()); + this.put(layout.h(hash), height); + this.put(layout.e(hash), entry.toRaw()); this.cacheHash.set(entry.hash, entry); if (!connect) { - return this.saveBlock(block, view, batch, false, function(err) { - if (err) + return this.saveBlock(block, view, false, function(err) { + if (err) { + self.drop(); return callback(err); - batch.write(callback); + } + self.commit(callback); }); } this.cacheHeight.set(entry.height, entry); - batch.put(layout.n(entry.prevBlock), hash); - batch.put(layout.H(entry.height), hash); - batch.put(layout.R, this.state.commit(hash)); + this.put(layout.n(entry.prevBlock), hash); + this.put(layout.H(entry.height), hash); - this.saveBlock(block, view, batch, true, function(err) { - if (err) + this.saveBlock(block, view, true, function(err) { + if (err) { + self.drop(); return callback(err); - batch.write(callback); + } + self.put(layout.R, self.pending.mark(hash)); + self.commit(callback); }); }; @@ -644,29 +731,34 @@ ChainDB.prototype.getTip = function getTip(callback) { */ ChainDB.prototype.reconnect = function reconnect(entry, block, view, callback) { - var batch = this.db.batch(); + var self = this; var hash = block.hash(); - batch.put(layout.n(entry.prevBlock), hash); - batch.put(layout.H(entry.height), hash); - batch.put(layout.R, this.state.commit(hash)); + this.start(); + + this.put(layout.n(entry.prevBlock), hash); + this.put(layout.H(entry.height), hash); this.cacheHash.set(entry.hash, entry); this.cacheHeight.set(entry.height, entry); if (this.options.spv) { - return batch.write(function(err) { + this.put(layout.R, this.pending.mark(hash)); + return this.commit(function(err) { if (err) return callback(err); return callback(null, entry, block); }); } - this.connectBlock(block, view, batch, function(err) { - if (err) + this.connectBlock(block, view, function(err) { + if (err) { + self.drop(); return callback(err); + } - batch.write(function(err) { + self.put(layout.R, self.pending.mark(hash)); + self.commit(function(err) { if (err) return callback(err); callback(null, entry, block); @@ -683,16 +775,16 @@ ChainDB.prototype.reconnect = function reconnect(entry, block, view, callback) { ChainDB.prototype.disconnect = function disconnect(entry, callback) { var self = this; - var batch = this.db.batch(); - batch.del(layout.n(entry.prevBlock)); - batch.del(layout.H(entry.height)); - batch.put(layout.R, this.state.commit(entry.prevBlock)); + this.start(); + this.del(layout.n(entry.prevBlock)); + this.del(layout.H(entry.height)); this.cacheHeight.remove(entry.height); if (this.options.spv) { - return batch.write(function(err) { + this.put(layout.R, this.pending.mark(entry.prevBlock)); + return this.commit(function(err) { if (err) return callback(err); callback(null, entry, entry.toHeaders()); @@ -700,17 +792,24 @@ ChainDB.prototype.disconnect = function disconnect(entry, callback) { } this.getBlock(entry.hash, function(err, block) { - if (err) + if (err) { + self.drop(); return callback(err); + } - if (!block) + if (!block) { + self.drop(); return callback(new Error('Block not found.')); + } - self.disconnectBlock(block, batch, function(err) { - if (err) + self.disconnectBlock(block, function(err) { + if (err) { + self.drop(); return callback(err); + } - batch.write(function(err) { + self.put(layout.R, self.pending.mark(entry.prevBlock)); + self.commit(function(err) { if (err) return callback(err); callback(null, entry, block); @@ -777,7 +876,6 @@ ChainDB.prototype.isMainChain = function isMainChain(hash, callback) { ChainDB.prototype.reset = function reset(block, callback) { var self = this; - var batch; this.get(block, function(err, entry) { if (err) @@ -800,23 +898,25 @@ ChainDB.prototype.reset = function reset(block, callback) { if (!tip) return callback(); - batch = self.db.batch(); + self.start(); if (tip.hash === entry.hash) { - batch.put(layout.R, self.state.commit(tip.hash)); - return batch.write(callback); + self.put(layout.R, self.pending.mark(tip.hash)); + return self.commit(callback); } - batch.del(layout.H(tip.height)); - batch.del(layout.h(tip.hash)); - batch.del(layout.e(tip.hash)); - batch.del(layout.n(tip.prevBlock)); + self.del(layout.H(tip.height)); + self.del(layout.h(tip.hash)); + self.del(layout.e(tip.hash)); + self.del(layout.n(tip.prevBlock)); - self.removeBlock(tip.hash, batch, function(err) { - if (err) + self.removeBlock(tip.hash, function(err) { + if (err) { + self.drop(); return callback(err); + } - batch.write(function(err) { + self.commit(function(err) { if (err) return next(err); self.get(tip.prevBlock, next); @@ -850,32 +950,30 @@ ChainDB.prototype.has = function has(height, callback) { * Save a block (not an entry) to the * database and potentially connect the inputs. * @param {Block} block - * @param {Batch} batch * @param {Boolean} connect - Whether to connect the inputs. * @param {Function} callback - Returns [Error, {@link Block}]. */ -ChainDB.prototype.saveBlock = function saveBlock(block, view, batch, connect, callback) { +ChainDB.prototype.saveBlock = function saveBlock(block, view, connect, callback) { if (this.options.spv) return utils.asyncify(callback)(null, block); - batch.put(layout.b(block.hash()), block.toRaw()); + this.put(layout.b(block.hash()), block.toRaw()); if (!connect) return utils.asyncify(callback)(null, block); - this.connectBlock(block, view, batch, callback); + this.connectBlock(block, view, callback); }; /** * Remove a block (not an entry) to the database. * Disconnect inputs. * @param {Block|Hash} block - {@link Block} or hash. - * @param {Batch} batch * @param {Function} callback - Returns [Error, {@link Block}]. */ -ChainDB.prototype.removeBlock = function removeBlock(hash, batch, callback) { +ChainDB.prototype.removeBlock = function removeBlock(hash, callback) { var self = this; this.getBlock(hash, function(err, block) { @@ -885,23 +983,22 @@ ChainDB.prototype.removeBlock = function removeBlock(hash, batch, callback) { if (!block) return callback(); - batch.del(layout.b(block.hash())); + self.del(layout.b(block.hash())); if (self.options.spv) return callback(null, block); - self.disconnectBlock(block, batch, callback); + self.disconnectBlock(block, callback); }); }; /** * Connect block inputs. * @param {Block} block - * @param {Batch} batch * @param {Function} callback - Returns [Error, {@link Block}]. */ -ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callback) { +ChainDB.prototype.connectBlock = function connectBlock(block, view, callback) { var undo = new BufferWriter(); var i, j, tx, input, output, prev, hashes, address, hash, coins, raw; @@ -910,23 +1007,23 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb // Genesis block's coinbase is unspendable. if (this.chain.isGenesis(block)) { - this.state.connect(block); + this.pending.connect(block); return utils.asyncify(callback)(null, block); } - this.state.connect(block); + this.pending.connect(block); for (i = 0; i < block.txs.length; i++) { tx = block.txs[i]; hash = tx.hash(); if (this.options.indexTX) { - batch.put(layout.t(hash), tx.toExtended()); + this.put(layout.t(hash), tx.toExtended()); if (this.options.indexAddress) { hashes = tx.getHashes(); for (j = 0; j < hashes.length; j++) { address = hashes[j]; - batch.put(layout.T(address, hash), DUMMY); + this.put(layout.T(address, hash), DUMMY); } } } @@ -943,13 +1040,13 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb address = input.getHash(); if (address) { prev = input.prevout; - batch.del(layout.C(address, prev.hash, prev.index)); + this.del(layout.C(address, prev.hash, prev.index)); } } input.coin.toRaw(undo); - this.state.spend(input.coin); + this.pending.spend(input.coin); } for (j = 0; j < tx.outputs.length; j++) { @@ -961,10 +1058,10 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb if (this.options.indexAddress) { address = output.getHash(); if (address) - batch.put(layout.C(address, hash, j), DUMMY); + this.put(layout.C(address, hash, j), DUMMY); } - this.state.add(output); + this.pending.add(output); } } @@ -973,19 +1070,19 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb for (i = 0; i < view.length; i++) { coins = view[i]; if (coins.size() === 0) { - batch.del(layout.c(coins.hash)); + this.del(layout.c(coins.hash)); this.coinCache.remove(coins.hash); } else { raw = coins.toRaw(); - batch.put(layout.c(coins.hash), raw); + this.put(layout.c(coins.hash), raw); this.coinCache.set(coins.hash, raw); } } if (undo.written > 0) - batch.put(layout.u(block.hash()), undo.render()); + this.put(layout.u(block.hash()), undo.render()); - this._pruneBlock(block, batch, function(err) { + this._pruneBlock(block, function(err) { if (err) return callback(err); callback(null, block); @@ -995,11 +1092,10 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, batch, callb /** * Disconnect block inputs. * @param {Block|Hash} block - {@link Block} or hash. - * @param {Batch} batch * @param {Function} callback - Returns [Error, {@link Block}]. */ -ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callback) { +ChainDB.prototype.disconnectBlock = function disconnectBlock(block, callback) { var self = this; var i, j, tx, input, output, prev, hashes, address, hash, coins, raw; @@ -1010,19 +1106,19 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callb if (err) return callback(err); - self.state.disconnect(block); + self.pending.disconnect(block); for (i = block.txs.length - 1; i >= 0; i--) { tx = block.txs[i]; hash = tx.hash('hex'); if (self.options.indexTX) { - batch.del(layout.t(hash)); + self.del(layout.t(hash)); if (self.options.indexAddress) { hashes = tx.getHashes(); for (j = 0; j < hashes.length; j++) { address = hashes[j]; - batch.del(layout.T(address, hash)); + self.del(layout.T(address, hash)); } } } @@ -1039,11 +1135,11 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callb address = input.getHash(); if (address) { prev = input.prevout; - batch.put(layout.C(address, prev.hash, prev.index), DUMMY); + self.put(layout.C(address, prev.hash, prev.index), DUMMY); } } - self.state.add(input.coin); + self.pending.add(input.coin); } // Add all of the coins we are about to @@ -1060,13 +1156,13 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callb if (self.options.indexAddress) { address = output.getHash(); if (address) - batch.del(layout.C(address, hash, j)); + self.del(layout.C(address, hash, j)); } // Spend added coin. view.spend(hash, j); - self.state.spend(output); + self.pending.spend(output); } } @@ -1075,16 +1171,16 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(block, batch, callb for (i = 0; i < view.length; i++) { coins = view[i]; if (coins.size() === 0) { - batch.del(layout.c(coins.hash)); + self.del(layout.c(coins.hash)); self.coinCache.remove(coins.hash); } else { raw = coins.toRaw(); - batch.put(layout.c(coins.hash), raw); + self.put(layout.c(coins.hash), raw); self.coinCache.set(coins.hash, raw); } } - batch.del(layout.u(block.hash())); + self.del(layout.u(block.hash())); callback(null, block); }); @@ -1628,11 +1724,10 @@ ChainDB.prototype.hasCoins = function hasCoins(hash, callback) { * add current block to the prune queue. * @private * @param {Block} - * @param {Batch} batch * @param {Function} callback */ -ChainDB.prototype._pruneBlock = function _pruneBlock(block, batch, callback) { +ChainDB.prototype._pruneBlock = function _pruneBlock(block, callback) { var futureHeight, key; if (this.options.spv) @@ -1646,7 +1741,7 @@ ChainDB.prototype._pruneBlock = function _pruneBlock(block, batch, callback) { futureHeight = block.height + this.keepBlocks; - batch.put(layout.q(futureHeight), block.hash()); + this.put(layout.q(futureHeight), block.hash()); key = layout.q(block.height); @@ -1660,9 +1755,9 @@ ChainDB.prototype._pruneBlock = function _pruneBlock(block, batch, callback) { if (!hash) return callback(); - batch.del(key); - batch.del(layout.b(hash)); - batch.del(layout.u(hash)); + self.del(key); + self.del(layout.b(hash)); + self.del(layout.u(hash)); callback(); }); @@ -1675,6 +1770,15 @@ function ChainState() { this.value = 0; } +ChainState.prototype.clone = function clone() { + var state = new ChainState(); + state.hash = this.hash; + state.tx = this.tx; + state.coin = this.coin; + state.value = this.value; + return state; +}; + ChainState.prototype.connect = function connect(block) { this.tx += block.txs.length; }; @@ -1693,7 +1797,7 @@ ChainState.prototype.spend = function spend(coin) { this.value -= coin.value; }; -ChainState.prototype.commit = function commit(hash) { +ChainState.prototype.mark = function mark(hash) { this.hash = hash; if (typeof this.hash !== 'string') this.hash = this.hash.toString('hex');