diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js deleted file mode 100644 index 149d2feb..00000000 --- a/lib/bcoin/chain.js +++ /dev/null @@ -1,842 +0,0 @@ -/** - * chain.js - blockchain management for bcoin - * Copyright (c) 2014-2015, Fedor Indutny (MIT License) - * https://github.com/indutny/bcoin - */ - -var inherits = require('inherits'); -var EventEmitter = require('events').EventEmitter; - -var bcoin = require('../bcoin'); -var constants = bcoin.protocol.constants; -var network = bcoin.protocol.network; -var utils = bcoin.utils; -var assert = utils.assert; - -/** - * Chain - */ - -function Chain(options) { - var preload = network.preload; - - if (!(this instanceof Chain)) - return new Chain(options); - - EventEmitter.call(this); - - this.options = options || {}; - this.prefix = 'bt/chain/'; - this.storage = this.options.storage; - this.strict = this.options.strict || false; - this.cacheLimit = this.options.cacheLimit || 2000; - - this.tip = null; - - this.block = { - list: [], - // Bloom filter for all known blocks - bloom: new bcoin.bloom(8 * 1024 * 1024, 16, 0xdeadbeef) - }; - - this.orphan = { - map: {}, - bmap: {}, - count: 0 - }; - - this.index = { - hashes: [], - ts: [], - heights: [], - lookup: {}, - lastTs: 0 - }; - - this.request = new utils.RequestCache(); - - this.fromJSON(preload); - - // Last TS after preload, needed for fill percent - this.index.lastTs = this.index.ts[this.index.ts.length - 1]; - - bcoin.chain.global = this; - - this.loading = false; - this._init(); -} - -inherits(Chain, EventEmitter); - -Chain.codes = { - okay: 0, - newOrphan: 1, - knownOrphan: 2, - forked: 3, - invalid: 4, - badCheckpoint: 4, - unchanged: 5 -}; - -Chain.messages = { - 0: 'Block was added successfully', - 1: 'Block is a new orphan', - 2: 'Block is a known orphan', - 3: 'Block is a greater fork', - 4: 'Block verification failed', - 5: 'Block does not match checkpoint', - 6: 'Chain is unchanged' -}; - -Chain.msg = function msg(code) { - return new Error(Chain.messages[code] || 'Unknown'); -}; - -function compareTs(a, b) { - return a -b; -} - -Chain.prototype._init = function _init() { - var self = this; - var s; - - if (!this.storage) - return; - - utils.nextTick(function() { - self.emit('debug', 'Chain is loading.'); - }); - - this.loading = true; - - s = this.storage.createReadStream({ - start: this.prefix, - end: this.prefix + 'z' - }); - - s.on('data', function(data) { - var hash = data.key.slice(self.prefix.length); - self._addIndex(hash, data.value.ts, data.value.height); - }); - - s.on('error', function(err) { - self.emit('error', err); - }); - - s.on('end', function() { - self.loading = false; - self.emit('load'); - self.emit('debug', 'Chain successfully loaded.'); - }); -}; - -Chain.prototype._getRange = function _getRange(hash, ts, futureOnly) { - var pos = utils.binaryInsert(this.index.ts, ts, compareTs, true); - var start = Math.min(Math.max(0, pos), this.index.ts.length - 1); - var curr, wnd, end; - - while (start > 0 && this.index.ts[start] > ts) - start--; - - curr = this.index.ts[start]; - wnd = 2 * 3600; - - if (!futureOnly) - while (start > 0 && this.index.ts[start] + wnd > curr) - start--; - - end = Math.min(Math.max(0, pos), this.index.ts.length - 1); - while (end < this.index.ts.length - 1 && this.index.ts[end] - wnd < ts) - end++; - - return { start: start, end: end }; -}; - -Chain.prototype._probeIndex = function _probeIndex(hash, ts) { - if (this.index.lookup[hash] == null) - return false; - - var start = 0; - var end = this.index.ts.length; - var range, i; - - if (ts) { - range = this._getRange(hash, ts); - start = range.start; - end = range.end; - } - - for (i = start; i <= end; i++) - if (this.index.hashes[i] === hash) - return { i: i, height: this.index.heights[i], ts: this.index.ts[i] }; - - return false; -}; - -Chain.prototype._addIndex = function _addIndex(hash, ts, height) { - var self = this; - - // Already added - if (this._probeIndex(hash, ts)) - return Chain.codes.unchanged; - - var pos = utils.binaryInsert(this.index.ts, ts, compareTs, true); - var checkpoint; - - // Duplicate height - if (this.index.hashes[pos] === hash - || this.index.hashes[pos - 1] === hash - || this.index.hashes[pos + 1] === hash) { - return Chain.codes.unchanged; - } - - // Fork at checkpoint - checkpoint = network.checkpoints[height]; - if (checkpoint) { - this.emit('checkpoint', height, hash, checkpoint); - if (hash !== checkpoint) { - this.emit('fork', height, hash, checkpoint); - return Chain.codes.badCheckpoint; - } - } - - this.index.ts.splice(pos, 0, ts); - this.index.hashes.splice(pos, 0, hash); - this.index.heights.splice(pos, 0, height); - - this.index.lookup[hash] = pos; - this.index.lookup[height] = pos; - - this.tip = this.getTip(); - this.emit('tip', this.tip); - - this._save(hash, { - ts: ts, - height: height - }); - - return Chain.codes.okay; -}; - -Chain.prototype.resetLastCheckpoint = function resetLastCheckpoint(height) { - var lastHeight = Object.keys(network.checkpoints).sort().indexOf(height) - 1; - - if (lastHeight < 0) - lastHeight = 0; - - this.resetHeight(lastHeight); -}; - -Chain.prototype.resetHeight = function resetHeight(height) { - var self = this; - var index = this.index.lookup[height]; - var ahead = this.index.hashes.slice(index + 1); - - assert(index != null); - - this.block.list.length = 0; - this.block.bloom.reset(); - this.orphan.map = {}; - this.orphan.bmap = {}; - this.orphan.count = 0; - this.index.ts.length = index + 1; - this.index.hashes.length = index + 1; - this.index.heights.length = index + 1; - this.index.lookup = {}; - this.index.hashes.forEach(function(hash, i) { - self.index.lookup[self.index.hashes[i]] = i; - self.index.lookup[self.index.heights[i]] = i; - }); - - this.index.lastTs = Math.min( - this.index.lastTs, - this.index.ts[this.index.ts.length - 1] - ); - - this.tip = this.getTip(); - this.emit('tip', this.tip); - - ahead.forEach(function(hash) { - self._delete(hash); - }); -}; - -Chain.prototype.resetTime = function resetTime(ts) { - var entry = this.byTime(ts); - if (!entry) - return; - return this.resetHeight(entry.height); -}; - -Chain.prototype._killFork = function _killFork(probe) { - var self = this; - var delta = 2 * 3600; - var upper = probe.ts + delta; - var lower = probe.ts - delta; - var index, i, len, hash; - - // Search duplicate heights down - index = -1; - for (i = probe.i - 1; i > 0 && this.index.ts[i] > lower; i--) { - if (probe.height === this.index.heights[i]) { - index = i; - break; - } - } - - // And up - if (index === -1) { - len = this.index.ts.length; - for (i = probe.i + 1; i < len && this.index.ts[i] < upper; i++) { - if (probe.height === this.index.heights[i]) { - index = i; - break; - } - } - } - - if (index === -1) - return false; - - hash = this.index.hashes[index]; - this.index.hashes.splice(index, 1); - this.index.ts.splice(index, 1); - this.index.heights.splice(index, 1); - - delete this.index.lookup[this.index.hashes[index]]; - delete this.index.lookup[this.index.heights[index]]; - - this.tip = this.getTip(); - this.emit('tip', this.tip); - - // Delete both blocks, let's see what others will choose - this._delete(hash); - - return true; -}; - -Chain.prototype.add = function add(block, peer) { - if (this.loading) { - this.once('load', function() { - this.add(block); - }); - return; - } - - var initial = block; - var code = Chain.codes.unchanged; - var hash, prev, prevProbe, range, hashes; - - for (;;) { - // Only validate the initial block (orphans were already validated) - if (block === initial && !block.verify()) { - code = Chain.codes.invalid; - break; - } - - hash = block.hash('hex'); - prev = block.prevBlock; - - // If the block is already known to be an orphan - if (this.orphan.map[prev]) { - code = Chain.codes.knownOrphan; - break; - } - - prevProbe = this._probeIndex(prev, block.ts); - - // Remove forked nodes from storage, if shorter chain is detected - if (this._killFork(prevProbe)) { - code = Chain.codes.forked; - break; - } - - // If previous block wasn't ever seen - add current to orphans - if (!this._probeIndex(hash, block.ts) && !prevProbe) { - this.orphan.count++; - this.orphan.map[prev] = block; - this.orphan.bmap[hash] = block; - - range = this._getRange(hash, block.ts, true); - hashes = this.index.hashes.slice(range.start, range.end + 1); - - // this.emit('missing', prev, hashes, block); - code = Chain.codes.newOrphan; - break; - } - - // Validated known block at this point - add it to index - if (prevProbe) { - code = this._addIndex(hash, block.ts, prevProbe.height + 1); - this.emit('block', block, peer); - } - - // At least one block was added - this.block.list.push(block); - this.block.bloom.add(hash, 'hex'); - - // Fullfill request - this.request.fullfill(hash, block); - - if (!this.orphan.map[hash]) - break; - - // We have orphan child for this block - add it to chain - block = this.orphan.map[hash]; - delete this.orphan.bmap[block.hash('hex')]; - delete this.orphan.map[hash]; - this.orphan.count--; - } - - // Failsafe for large orphan chains - if (this.orphan.count > 10000) { - this.orphan.map = {}; - this.orphan.bmap = {}; - this.orphan.count = 0; - } - - // No need to have a huge chain - // if (this.size() > 100000) - // this.compact(); - - // Compress old blocks - this._compress(); - - return code; -}; - -Chain.prototype._compress = function compress() { - var i; - - // Keep at least 1000 blocks and at most 2000 by default - if (this.block.list.length < this.cacheLimit) - return; - - // Bloom filter rebuilt is needed - this.block.list = this.block.list.slice(-(this.cacheLimit / 2 | 0)); - this.block.bloom.reset(); - - for (i = 0; i < this.block.list.length; i++) - this.block.bloom.add(this.block.list[i].hash('hex'), 'hex'); -}; - -Chain.prototype.has = function has(hash, noIndex, cb) { - var i; - - if (typeof noIndex === 'function') { - cb = noIndex; - noIndex = false; - } - - if (this.loading) { - this.once('load', function() { - this.has(hash, noIndex, cb); - }); - return; - } - - cb = utils.asyncify(cb); - - if (this.hasCache(hash)) - return cb(true); - - if (this.hasOrphan(hash)) - return cb(true); - - if (!noIndex) { - if (this.hasBlock(hash)) - return cb(true); - } - - return cb(false); -}; - -Chain.prototype.byHash = function byHash(hash) { - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - var index = this.index.lookup[hash]; - - if (index == null) - return null; - - return { - index: index, - hash: this.index.hashes[index], - ts: this.index.ts[index], - height: this.index.heights[index] - }; -}; - -Chain.prototype.byHeight = function byHeight(height) { - var index = this.index.lookup[height]; - - if (index == null) - return null; - - return { - index: index, - hash: this.index.hashes[index], - ts: this.index.ts[index], - height: this.index.heights[index] - }; -}; - -Chain.prototype.byTime = function byTime(ts) { - for (var i = this.index.ts.length - 1; i >= 0; i--) { - if (ts >= this.index.ts[i]) - return this.byHeight(this.index.heights[i]); - } - return null; -}; - -Chain.prototype.hasBlock = function hasBlock(hash) { - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - return this.index.lookup[hash] != null; -}; - -Chain.prototype.hasOrphan = function hasOrphan(hash) { - return !!this.getOrphan(hash); -}; - -Chain.prototype.hasCache = function hasCache(hash) { - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - if (!this.block.bloom.test(hash, 'hex')) - return false; - - if (this.strict) - return !!this.getCache(hash); - - return true; -}; - -Chain.prototype.getBlock = function getBlock(hash) { - if (typeof hash === 'number') - return this.byHeight(hash); - return this.byHash(hash); -}; - -Chain.prototype.getOrphan = function getOrphan(hash) { - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - return this.orphan.bmap[hash] || null; -}; - -Chain.prototype.getCache = function getCache(hash) { - var i; - - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - for (i = 0; i < this.block.list.length; i++) { - if (this.block.list[i].hash('hex') === hash) - return this.block.list[i]; - } -}; - -Chain.prototype.getTip = function() { - var index = this.index.hashes.length - 1; - return { - index: index, - hash: this.index.hashes[index], - ts: this.index.ts[index], - height: this.index.heights[index] - }; -}; - -Chain.prototype.isFull = function isFull() { - // < 40m since last block - if (this.request.count) - return false; - - var delta = utils.now() - this.index.ts[this.index.ts.length - 1]; - - return delta < 40 * 60; -}; - -Chain.prototype.fillPercent = function fillPercent() { - var total = (utils.now() - 40 * 60) - this.index.ts[0]; - var current = this.index.ts[this.index.ts.length - 1] - this.index.ts[0]; - return Math.max(0, Math.min(current / total, 1)); -}; - -Chain.prototype.hashRange = function hashRange(start, end) { - var hashes; - - start = this.byTime(start); - end = this.byTime(end); - - if (!start || !end) - return []; - - hashes = this.index.hashes.slice(start.index, end.index + 1); - - if (!this.options.fullNode) - hashes = hashes.filter(Boolean); - - return hashes; -}; - -Chain.prototype.hashesInRange = function hashesInRange(start, end, cb) { - var ts, hashes, heights, zip, i, count; - - if (this.loading) { - this.once('load', function() { - this.hashesInRange(start, end, cb); - }); - return; - } - - cb = utils.asyncify(cb); - ts = this.index.ts; - - start = utils.binaryInsert(ts, start, compareTs, true); - if (start > 0 && ts[start - 1] >= start) - start--; - - end = utils.binaryInsert(ts, end, compareTs, true); - - // Zip hashes and heights together and sort them by height - hashes = this.index.hashes.slice(start, end); - heights = this.index.heights.slice(start, end); - zip = []; - - for (i = 0; i < hashes.length; i++) - zip.push({ hash: hashes[i], height: heights[i] }); - - zip = zip.sort(function(a, b) { - return a.height - b.height; - }); - - hashes = zip.map(function(a) { - return a.hash; - }); - - count = zip[zip.length - 1].height - zip[0].height + 1; - - return cb(hashes, count); -}; - -Chain.prototype.getStartHeight = function getStartHeight() { - return 0; -}; - -Chain.prototype.locatorHashes = function locatorHashes(obj) { - var start; - - if (obj) { - if (Array.isArray(obj)) - obj = utils.toHex(obj); - else if (obj.hash) - obj = obj.hash('hex'); - } - - // Convert the start to indexes - if (obj != null) { - if (typeof obj === 'string') { - start = this.byHash(obj); - if (start) { - start = start.index; - } else { - // return [obj]; - start = obj; - } - } else if (typeof obj === 'number') { - start = this.byHeight(obj); - if (start) - start = start.index; - } - - assert(start != null); - } - - return bcoin.fullChain.prototype.locatorHashes.call(this, start); -}; - -Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { - return bcoin.fullChain.prototype.getOrphanRoot.call(this, hash); -}; - -Chain.prototype.getHeight = function getHeight(hash) { - var entry = this.byHash(hash); - - if (!entry) - return -1; - - return entry.height; -}; - -Chain.prototype.getNextBlock = function getNextBlock(hash) { - var entry = this.byHeight(hash); - var nextHeight; - - if (!entry) - return null; - - nextHeight = this.index.heights[entry.index + 1]; - - if (nextHeight == null || nextHeight !== entry.height + 1) - return null; - - return this.index.hashes[entry.index + 1] || null; -}; - -Chain.prototype.size = function size() { - return this.index.hashes.length; -}; - -Chain.prototype.height = function height() { - return this.getTip().height; -}; - -Chain.prototype.target = function target(last) { - assert(false); -}; - -Chain.prototype.retarget = function retarget(last, firstTs) { - assert(false); -}; - -Chain.prototype.compact = function compact(keep) { - var index = this._compact(keep); - this.index.hashes = index.hashes; - this.index.ts = index.ts; - this.index.heights = index.heights; - this.index.lookup = {}; - this.index.hashes.forEach(function(hash, i) { - this.index.lookup[this.index.hashes[i]] = i; - this.index.lookup[this.index.heights[i]] = i; - }, this); -}; - -Chain.prototype._compact = function _compact(keep) { - keep = keep || 1000; - - // Keep only last 1000 consequent blocks, dilate others at: - // 7 day range for blocks before 2013 - // 12 hour for blocks before 2014 - // 6 hour for blocks in 2014 and after it - // (or at maximum 250 block range) - var last = { - hashes: this.index.hashes.slice(-keep), - ts: this.index.ts.slice(-keep), - heights: this.index.heights.slice(-keep) - }; - - var first = { - hashes: [], - ts: [], - heights: [] - }; - - var delta1 = 7 * 24 * 3600; - var delta2 = 12 * 3600; - var delta3 = 6 * 3600; - - var lastTs = 0; - var lastHeight = -1000; - var i, ts, delta, hdelta; - - for (i = 0; i < this.index.ts.length - keep; i++) { - ts = this.index.ts[i]; - - delta = ts < 1356984000 - ? delta1 - : ts < 1388520000 ? delta2 : delta3; - - hdelta = this.index.heights[i] - lastHeight; - - if (ts - lastTs < delta && hdelta < 250) - continue; - - lastTs = ts; - lastHeight = this.index.heights[i]; - first.hashes.push(this.index.hashes[i]); - first.ts.push(this.index.ts[i]); - first.heights.push(this.index.heights[i]); - } - - return { - hashes: first.hashes.concat(last.hashes), - ts: first.ts.concat(last.ts), - heights: first.heights.concat(last.heights) - }; -}; - -Chain.prototype._save = function(hash, obj) { - var self = this; - - if (!this.storage) - return; - - this.storage.put(this.prefix + hash, obj, function(err) { - if (err) - self.emit('error', err); - }); -}; - -Chain.prototype._delete = function(hash) { - var self = this; - - if (!this.storage) - return; - - this.storage.del(this.prefix + hash, function(err) { - if (err) - self.emit('error', err); - }); -}; - -Chain.prototype.toJSON = function toJSON() { - var index = this._compact(); - return { - v: 1, - type: 'chain', - network: network.type, - hashes: index.hashes, - ts: index.ts, - heights: index.heights - }; -}; - -Chain.prototype.fromJSON = function fromJSON(json) { - var i; - - assert.equal(json.v, 1); - assert.equal(json.type, 'chain'); - - if (json.network) - assert.equal(json.network, network.type); - - this.index.hashes = json.hashes.slice(); - this.index.ts = json.ts.slice(); - this.index.heights = json.heights.slice(); - - assert(this.index.hashes.length > 0); - - for (i = 0; i < this.index.hashes.length; i++) { - this.index.lookup[this.index.hashes[i]] = i; - this.index.lookup[this.index.heights[i]] = i; - } -}; - -/** - * Expose - */ - -module.exports = Chain;