diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 8fad723e..fa901c31 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -329,14 +329,10 @@ Chain.prototype._preload = function _preload(callback) { // Create a chain entry. entry = new bcoin.chainblock(self, data, lastEntry); - // Filthy hack to avoid writing - // redundant blocks to disk! - if (entry.height <= chainHeight) { + if (entry.height <= chainHeight) self.db.addCache(entry); - // self.db.bloom(entry.hash, 'hex'); - } else { - self.db.save(entry); - } + else + self.db.save(entry, null, true); if ((height + 1) % 50000 === 0) utils.debug('Received %d headers from electrum.org.', height + 1); @@ -588,11 +584,11 @@ Chain.prototype._checkDuplicates = function _checkDuplicates(block, prev, peer, // and extraNonce. if (result) { utils.debug('Block is overwriting txids: %s', block.rhash); - if (!(network.type === 'main' && (height === 91842 || height === 91880))) { - self.emit('verify-error', - block, 'invalid', 'bad-txns-BIP30', 100, peer); - return next(null, false); - } + if (network.type === 'main' && (height === 91842 || height === 91880)) + return next(null, true); + self.emit('verify-error', + block, 'invalid', 'bad-txns-BIP30', 100, peer); + return next(null, false); } next(null, true); @@ -867,34 +863,37 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) { }); }; +Chain.prototype._addEntry = function _addEntry(entry, block, callback) { + // Set main chain only if chainwork is higher. + // Add the block but do _not_ connect the inputs. + if (entry.chainwork.cmp(this.tip.chainwork) <= 0) { + return this.db.save(entry, block, false, function(err) { + if (err) + return callback(err); + + return callback(null, false); + }); + } + + // Attempt to add block to the chain index. + return this._setBestChain(entry, block, function(err) { + if (err) + return callback(err); + + return callback(null, true); + }); +}; + Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) { var self = this; - callback = utils.asyncify(callback); - this.lastUpdate = utils.now(); - // Start fsyncing writes once we're no - // longer dealing with historical data. - if (this.isFull()) - this.db.fsync = true; - - if (!this.tip) { - if (entry.hash !== network.genesis.hash) - return callback(new Error('Bad genesis block.')); - - done(); - } else if (entry.prevBlock === this.tip.hash) { - done(); - } else { - self._reorganize(entry, block, done); - } - function done(err) { if (err) return callback(err); - self.db.save(entry, block, function(err) { + self.db.save(entry, block, true, function(err) { if (err) return callback(err); @@ -909,6 +908,17 @@ Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) { return callback(); }); } + + if (!this.tip) { + if (entry.hash !== network.genesis.hash) + return utils.asyncify(callback)(new Error('Bad genesis block.')); + + done(); + } else if (entry.prevBlock === this.tip.hash) { + done(); + } else { + self._reorganize(entry, block, done); + } }; Chain.prototype.reset = function reset(height, callback, force) { @@ -1203,12 +1213,8 @@ Chain.prototype.add = function add(initial, peer, callback, force) { height: height }, prev); - // Set main chain only if chainwork is higher. - if (entry.chainwork.cmp(self.tip.chainwork) <= 0) - return done(); - // Attempt to add block to the chain index. - self._setBestChain(entry, block, function(err) { + self._addEntry(entry, block, function(err, mainChain) { if (err) return done(err); @@ -1217,8 +1223,10 @@ Chain.prototype.add = function add(initial, peer, callback, force) { total++; // Emit our block (and potentially resolved - // orphan) so the programmer can save it. - self.emit('block', block, entry, peer); + // orphan) only if it is on the main chain. + if (mainChain) + self.emit('block', block, entry, peer); + if (block.hash('hex') !== initial.hash('hex')) self.emit('resolved', block, entry, peer); diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 41bef0d1..e6174e2d 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -56,59 +56,6 @@ function ChainDB(chain, options) { this._init(); } -function DumbCache(size) { - this.data = {}; - this.count = 0; - this.size = size; -} - -DumbCache.prototype.set = function set(key, value) { - key = key + ''; - - assert(value !== undefined); - - if (this.count > this.size) - this.reset(); - - if (this.data[key] === undefined) - this.count++; - - this.data[key] = value; -}; - -DumbCache.prototype.remove = function remove(key) { - key = key + ''; - - if (this.data[key] === undefined) - return; - - this.count--; - delete this.data[key]; -}; - -DumbCache.prototype.get = function get(key) { - key = key + ''; - return this.data[key]; -}; - -DumbCache.prototype.has = function has(key) { - key = key + ''; - return this.data[key] !== undefined; -}; - -DumbCache.prototype.reset = function reset() { - this.data = {}; - this.count = 0; -}; - -function NullCache(size) {} - -NullCache.prototype.set = function set(key, value) {}; -NullCache.prototype.remove = function remove(key) {}; -NullCache.prototype.get = function get(key) {}; -NullCache.prototype.has = function has(key) {}; -NullCache.prototype.reset = function reset() {}; - utils.inherits(ChainDB, EventEmitter); ChainDB.prototype._init = function _init() { @@ -162,7 +109,7 @@ ChainDB.prototype._init = function _init() { block = bcoin.block.fromRaw(network.genesisBlock, 'hex'); block.height = 0; - self.save(genesis, block, finish); + self.save(genesis, block, true, finish); }); }); }; @@ -206,6 +153,8 @@ ChainDB.prototype.getCache = function getCache(hash) { }; ChainDB.prototype.getHeight = function getHeight(hash, callback) { + callback = utils.asyncify(callback); + if (hash == null || hash < 0) return callback(null, -1); @@ -231,6 +180,8 @@ ChainDB.prototype.getHeight = function getHeight(hash, callback) { }; ChainDB.prototype.getHash = function getHash(height, callback) { + callback = utils.asyncify(callback); + if (height == null || height < 0) return callback(null, null); @@ -305,7 +256,7 @@ ChainDB.prototype.getBoth = function getBoth(block, callback) { var hash, height; if (block == null || block < 0) - return callback(null, null, -1); + return utils.asyncify(callback)(null, null, -1); if (typeof block === 'string') hash = block; @@ -340,7 +291,7 @@ ChainDB.prototype._getEntry = function _getEntry(hash, callback) { var entry; if (hash == null || hash < 0) - return callback(); + return utils.nextTick(callback); return this.getBoth(hash, function(err, hash, height) { if (err && err.type !== 'NotFoundError') @@ -369,8 +320,6 @@ ChainDB.prototype._getEntry = function _getEntry(hash, callback) { ChainDB.prototype.get = function get(height, callback) { var self = this; - callback = utils.asyncify(callback); - return this._getEntry(height, function(err, entry) { if (err) return callback(err); @@ -378,35 +327,52 @@ ChainDB.prototype.get = function get(height, callback) { if (!entry) return callback(); - self.addCache(entry); + // There's no efficient way to check whether + // this is in the main chain or not, so + // don't add it to the height cache. + self.cacheHash.set(entry.hash, entry); return callback(null, entry); }); }; -ChainDB.prototype.save = function save(entry, block, callback) { +ChainDB.prototype.save = function save(entry, block, connect, callback) { var self = this; - var batch, height; + var batch, hash, height; - callback = utils.asyncify(callback); + callback = utils.ensure(callback); assert(entry.height >= 0); - this.addCache(entry); - batch = this.db.batch(); + + hash = new Buffer(entry.hash, 'hex'); + height = new Buffer(4); utils.writeU32(height, entry.height, 0); batch.put('c/b/' + entry.hash, height); batch.put('c/c/' + entry.hash, entry.toRaw()); - batch.put('c/n/' + entry.prevBlock, new Buffer(entry.hash, 'hex')); - batch.put('c/h/' + pad32(entry.height), new Buffer(entry.hash, 'hex')); - batch.put('c/t', new Buffer(entry.hash, 'hex')); + batch.put('c/n/' + entry.prevBlock, hash); + + this.cacheHash.set(entry.hash, entry); + + if (!connect) { + return this.saveBlock(block, batch, false, function(err) { + if (err) + return callback(err); + return batch.write(callback); + }); + } + + this.cacheHeight.set(entry.height, entry); + + batch.put('c/h/' + pad32(entry.height), hash); + batch.put('c/t', hash); this.emit('add entry', entry); - this.saveBlock(block, batch, function(err) { + this.saveBlock(block, batch, true, function(err) { if (err) return callback(err); @@ -435,7 +401,7 @@ ChainDB.prototype.connect = function connect(block, callback) { var self = this; var batch; - this._get(block, function(err, entry) { + this._ensureEntry(block, function(err, entry) { if (err) return callback(err); @@ -468,7 +434,7 @@ ChainDB.prototype.disconnect = function disconnect(block, callback) { var self = this; var batch; - this._get(block, function(err, entry) { + this._ensureEntry(block, function(err, entry) { if (err) return callback(err); @@ -497,7 +463,7 @@ ChainDB.prototype.disconnect = function disconnect(block, callback) { }); }; -ChainDB.prototype._get = function _get(block, callback) { +ChainDB.prototype._ensureEntry = function _ensureEntry(block, callback) { if (block instanceof bcoin.chainblock) return callback(null, block); return this.get(block, callback); @@ -571,7 +537,7 @@ ChainDB.prototype.reset = function reset(block, callback) { ChainDB.prototype.has = function has(height, callback) { if (height == null || height < 0) - return callback(null, false); + return utils.asyncify(callback)(null, false); return this.getBoth(height, function(err, hash, height) { if (err) @@ -580,9 +546,9 @@ ChainDB.prototype.has = function has(height, callback) { }); }; -ChainDB.prototype.saveBlock = function saveBlock(block, batch, callback) { +ChainDB.prototype.saveBlock = function saveBlock(block, batch, connect, callback) { if (this.options.spv) - return callback(); + return utils.nextTick(callback); batch.put('b/b/' + block.hash('hex'), block.toCompact()); @@ -590,6 +556,9 @@ ChainDB.prototype.saveBlock = function saveBlock(block, batch, callback) { batch.put('t/t/' + tx.hash('hex'), tx.toExtended()); }); + if (!connect) + return utils.nextTick(callback); + this.connectBlock(block, batch, callback); }; @@ -597,9 +566,9 @@ ChainDB.prototype.removeBlock = function removeBlock(hash, batch, callback) { var self = this; if (this.options.spv) - return callback(); + return utils.nextTick(callback); - this._getTXBlock(hash, function(err, block) { + this._ensureHistory(hash, function(err, block) { if (err) return callback(err); @@ -621,10 +590,10 @@ ChainDB.prototype.connectBlock = function connectBlock(block, batch, callback) { if (this.options.spv) { self.emit('add block', block); - return callback(); + return utils.nextTick(callback); } - this._getCoinBlock(block, function(err, block) { + this._ensureBlock(block, function(err, block) { var height; if (err) @@ -638,6 +607,7 @@ ChainDB.prototype.connectBlock = function connectBlock(block, batch, callback) { var uniq = {}; tx.inputs.forEach(function(input) { + var key = input.prevout.hash + '/' + input.prevout.index; var address; if (tx.isCoinbase()) @@ -653,18 +623,15 @@ ChainDB.prototype.connectBlock = function connectBlock(block, batch, callback) { batch.put('t/a/' + address + '/' + hash, DUMMY); } - if (address) { - batch.del( - 'u/a/' + address - + '/' + input.prevout.hash - + '/' + input.prevout.index); - } + if (address) + batch.del('u/a/' + address + '/' + key); } - batch.del('u/t/' + input.prevout.hash + '/' + input.prevout.index); + batch.del('u/t/' + key); }); tx.outputs.forEach(function(output, i) { + var key = hash + '/' + i; var address; if (self.options.indexAddress) { @@ -676,10 +643,10 @@ ChainDB.prototype.connectBlock = function connectBlock(block, batch, callback) { } if (address) - batch.put('u/a/' + address + '/' + hash + '/' + i, DUMMY); + batch.put('u/a/' + address + '/' + key, DUMMY); } - batch.put('u/t/' + hash + '/' + i, bcoin.coin(tx, i).toRaw()); + batch.put('u/t/' + key, bcoin.coin(tx, i).toRaw()); }); }); @@ -693,9 +660,9 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba var self = this; if (this.options.spv) - return callback(); + return utils.nextTick(callback); - this._getTXBlock(hash, function(err, block) { + this._ensureHistory(hash, function(err, block) { if (err) return callback(err); @@ -710,6 +677,7 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba var uniq = {}; tx.inputs.forEach(function(input) { + var key = input.prevout.hash + '/' + input.prevout.index; var address; if (tx.isCoinbase()) @@ -725,21 +693,15 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba batch.del('t/a/' + address + '/' + hash); } - if (address) { - batch.put('u/a/' + address - + '/' + input.prevout.hash - + '/' + input.prevout.index, - DUMMY); - } + if (address) + batch.put('u/a/' + address + '/' + key, DUMMY); } - batch.put('u/t/' - + input.prevout.hash - + '/' + input.prevout.index, - input.coin.toRaw()); + batch.put('u/t/' + key, input.coin.toRaw()); }); tx.outputs.forEach(function(output, i) { + var key = hash + '/' + i; var address; if (self.options.indexAddress) { @@ -751,10 +713,10 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba } if (address) - batch.del('u/a/' + address + '/' + hash + '/' + i); + batch.del('u/a/' + address + '/' + key); } - batch.del('u/t/' + hash + '/' + i); + batch.del('u/t/' + key); }); }); @@ -767,8 +729,6 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba ChainDB.prototype.fillCoins = function fillCoins(tx, callback) { var self = this; - callback = utils.asyncify(callback); - if (Array.isArray(tx)) { return utils.forEachSerial(tx, function(tx, next) { self.fillCoins(tx, next); @@ -780,7 +740,7 @@ ChainDB.prototype.fillCoins = function fillCoins(tx, callback) { } if (tx.isCoinbase()) - return callback(null, tx); + return utils.asyncify(callback)(null, tx); utils.forEachSerial(tx.inputs, function(input, next) { if (input.coin) @@ -805,8 +765,6 @@ ChainDB.prototype.fillCoins = function fillCoins(tx, callback) { ChainDB.prototype.fillTX = function fillTX(tx, callback) { var self = this; - callback = utils.asyncify(callback); - if (Array.isArray(tx)) { return utils.forEachSerial(tx, function(tx, next) { self.fillTX(tx, next); @@ -818,7 +776,7 @@ ChainDB.prototype.fillTX = function fillTX(tx, callback) { } if (tx.isCoinbase()) - return callback(null, tx); + return utils.asyncify(callback)(null, tx); if (this.prune) { return utils.forEachSerial(tx.inputs, function(input, next) { @@ -935,15 +893,15 @@ ChainDB.prototype.getCoinsByAddress = function getCoinsByAddress(addresses, opti }; ChainDB.prototype.getCoin = function getCoin(hash, index, callback) { - var id = 'u/t/' + hash + '/' + index; + var key = 'u/t/' + hash + '/' + index; var coin; - this.db.get(id, function(err, data) { - if (err) { - if (err.type === 'NotFoundError') - return callback(); + this.db.get(key, function(err, data) { + if (err && err.type !== 'NotFoundError') return callback(err); - } + + if (!data) + return callback(); try { coin = bcoin.coin.fromRaw(data); @@ -1036,10 +994,10 @@ ChainDB.prototype.getTXByAddress = function getTXByAddress(addresses, options, c ChainDB.prototype.getTX = function getTX(hash, callback) { var self = this; - var id = 't/t/' + hash; + var key = 't/t/' + hash; var tx; - this.db.get(id, function(err, data) { + this.db.get(key, function(err, data) { if (err) { if (err.type === 'NotFoundError') return callback(); @@ -1097,11 +1055,11 @@ ChainDB.prototype.getFullBlock = function getFullBlock(hash, callback) { }); }; -ChainDB.prototype._getCoinBlock = function _getCoinBlock(hash, callback) { +ChainDB.prototype._ensureBlock = function _ensureBlock(hash, callback) { var self = this; if (hash instanceof bcoin.block) - return callback(null, hash); + return utils.asyncify(callback)(null, hash); return this.getBlock(hash, function(err, block) { if (err) @@ -1114,11 +1072,11 @@ ChainDB.prototype._getCoinBlock = function _getCoinBlock(hash, callback) { }); }; -ChainDB.prototype._getTXBlock = function _getTXBlock(hash, callback) { +ChainDB.prototype._ensureHistory = function _ensureHistory(hash, callback) { var self = this; if (hash instanceof bcoin.block) - return callback(null, hash); + return utils.asyncify(callback)(null, hash); return this.getBlock(hash, function(err, block) { if (err) @@ -1133,7 +1091,7 @@ ChainDB.prototype._getTXBlock = function _getTXBlock(hash, callback) { ChainDB.prototype.fillBlock = function fillBlock(block, callback) { return this.fillCoins(block.txs, function(err) { - var coins, i, tx, hash, j, input, id; + var coins, i, tx, hash, j, input, key; if (err) return callback(err); @@ -1146,10 +1104,10 @@ ChainDB.prototype.fillBlock = function fillBlock(block, callback) { for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; - id = input.prevout.hash + '/' + input.prevout.index; - if (!input.coin && coins[id]) { - input.coin = coins[id]; - delete coins[id]; + key = input.prevout.hash + '/' + input.prevout.index; + if (!input.coin && coins[key]) { + input.coin = coins[key]; + delete coins[key]; } } @@ -1163,7 +1121,7 @@ ChainDB.prototype.fillBlock = function fillBlock(block, callback) { ChainDB.prototype.fillTXBlock = function fillTXBlock(block, callback) { return this.fillTX(block.txs, function(err) { - var coins, i, tx, hash, j, input, id; + var coins, i, tx, hash, j, input, key; if (err) return callback(err); @@ -1176,10 +1134,10 @@ ChainDB.prototype.fillTXBlock = function fillTXBlock(block, callback) { for (j = 0; j < tx.inputs.length; j++) { input = tx.inputs[j]; - id = input.prevout.hash + '/' + input.prevout.index; - if (!input.coin && coins[id]) { - input.coin = coins[id]; - delete coins[id]; + key = input.prevout.hash + '/' + input.prevout.index; + if (!input.coin && coins[key]) { + input.coin = coins[key]; + delete coins[key]; } } @@ -1193,7 +1151,7 @@ ChainDB.prototype.fillTXBlock = function fillTXBlock(block, callback) { ChainDB.prototype.getBlock = function getBlock(hash, callback) { var self = this; - var id, block; + var key, block; return this.getHash(hash, function(err, hash) { if (err) @@ -1202,14 +1160,14 @@ ChainDB.prototype.getBlock = function getBlock(hash, callback) { if (!hash) return callback(); - id = 'b/b/' + hash; + key = 'b/b/' + hash; - self.db.get(id, function(err, data) { - if (err) { - if (err.type === 'NotFoundError') - return callback(); + self.db.get(key, function(err, data) { + if (err && errr.type !== 'NotFoundError') return callback(err); - } + + if (!data) + return callback(); try { block = bcoin.block.fromCompact(data); @@ -1344,24 +1302,24 @@ ChainDB.prototype._pruneQueue = function _pruneQueue(block, batch, callback) { hash = utils.toHex(hash); - self.db.get('b/b/' + hash, function(err, cblock) { + self.db.get('b/b/' + hash, function(err, compact) { if (err && err.type !== 'NotFoundError') return callback(err); batch.del(key); - if (!cblock) + if (!compact) return callback(); try { - cblock = bcoin.block.fromCompact(cblock); + compact = bcoin.block.fromCompact(compact); } catch (e) { return callback(e); } batch.del('b/b/' + hash); - cblock.hashes.forEach(function(hash) { + compact.hashes.forEach(function(hash) { batch.del('t/t/' + hash); }); @@ -1407,10 +1365,10 @@ ChainDB.prototype._pruneCoinQueue = function _pruneQueue(block, batch, callback) ChainDB.prototype._getPruneCoin = function _getPruneCoin(hash, index, callback) { var self = this; - var id = 'u/x/' + hash + '/' + index; + var key = 'u/x/' + hash + '/' + index; var coin; - this.db.get(id, function(err, data) { + this.db.get(key, function(err, data) { if (err && err.type !== 'NotFoundError') return callback(err); @@ -1429,6 +1387,59 @@ ChainDB.prototype._getPruneCoin = function _getPruneCoin(hash, index, callback) }); }; +function DumbCache(size) { + this.data = {}; + this.count = 0; + this.size = size; +} + +DumbCache.prototype.set = function set(key, value) { + key = key + ''; + + assert(value !== undefined); + + if (this.count > this.size) + this.reset(); + + if (this.data[key] === undefined) + this.count++; + + this.data[key] = value; +}; + +DumbCache.prototype.remove = function remove(key) { + key = key + ''; + + if (this.data[key] === undefined) + return; + + this.count--; + delete this.data[key]; +}; + +DumbCache.prototype.get = function get(key) { + key = key + ''; + return this.data[key]; +}; + +DumbCache.prototype.has = function has(key) { + key = key + ''; + return this.data[key] !== undefined; +}; + +DumbCache.prototype.reset = function reset() { + this.data = {}; + this.count = 0; +}; + +function NullCache(size) {} + +NullCache.prototype.set = function set(key, value) {}; +NullCache.prototype.remove = function remove(key) {}; +NullCache.prototype.get = function get(key) {}; +NullCache.prototype.has = function has(key) {}; +NullCache.prototype.reset = function reset() {}; + /** * Expose */