diff --git a/lib/bcoin.js b/lib/bcoin.js index 419f9fd8..7f13baf4 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -21,6 +21,8 @@ bcoin.tx = require('./bcoin/tx'); bcoin.txPool = require('./bcoin/tx-pool'); bcoin.block = require('./bcoin/block'); bcoin.chain = require('./bcoin/chain'); +bcoin.spvChain = require('./bcoin/chain'); +bcoin.fullChain = require('./bcoin/fullchain'); bcoin.wallet = require('./bcoin/wallet'); bcoin.peer = require('./bcoin/peer'); bcoin.pool = require('./bcoin/pool'); diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 0aa0b8b7..4ef5d30a 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -267,7 +267,7 @@ Block.prototype.getNextBlock = function getNextBlock(chain) { chain = chain || bcoin.chain.global; if (!chain) - return utils.toHex(constants.protocol.zeroHash); + return utils.toHex(constants.zeroHash); next = chain.getNextBlock(this.hash('hex')); diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index f626d3b1..d66d8e39 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -53,24 +53,12 @@ function Chain(options) { this.request = new utils.RequestCache(); - // Start from the genesis block - // if we're a full node. - if (this.options.fullNode) { - preload = { - v: preload.v, - type: preload.type, - hashes: preload.hashes.slice(0, 1), - ts: preload.ts.slice(0, 1), - heights: preload.heights.slice(0, 1) - }; - } - this.fromJSON(preload); // Last TS after preload, needed for fill percent this.index.lastTs = this.index.ts[this.index.ts.length - 1]; - Chain.global = this; + bcoin.chain.global = this; this.loading = false; this._init(); @@ -338,7 +326,7 @@ Chain.prototype.add = function add(block) { // At least one block was added res = true; this.block.list.push(block); - this._bloomBlock(block); + this.block.bloom.add(hash, 'hex'); // Fullfill request this.request.fullfill(hash, block); @@ -371,11 +359,7 @@ Chain.prototype._compress = function compress() { this.block.bloom.reset(); for (i = 0; i < this.block.list.length; i++) - this._bloomBlock(this.block.list[i]); -}; - -Chain.prototype._bloomBlock = function _bloomBlock(block) { - this.block.bloom.add(block.hash(), 'hex'); + this.block.bloom.add(this.block.list[i].hash('hex'), 'hex'); }; Chain.prototype.has = function has(hash, noProbe, cb) { @@ -413,6 +397,45 @@ Chain.prototype.has = function has(hash, noProbe, cb) { return cb(!!this.orphan.map[hash]); }; +Chain.prototype.byHeight = function byHeight(height) { + if (this.loading) + return null; + + var index = this.index.heights.indexOf(height); + + if (index === -1) + return -1; + + return { + index: index, + hash: this.index.hashes[index], + ts: this.index.ts[index], + height: this.index.heights[index] + }; +}; + +Chain.prototype.byHash = function byHash(hash) { + if (this.loading) + return null; + + if (Array.isArray(hash)) + hash = utils.toHex(hash); + else if (hash.hash) + hash = hash.hash('hex'); + + var index = this.index.hashes.indexOf(hash); + + if (index === -1) + return -1; + + return { + index: index, + hash: this.index.hashes[index], + ts: this.index.ts[index], + height: this.index.heights[index] + }; +}; + Chain.prototype.hasBlock = function hasBlock(hash) { if (this.loading) return false; @@ -533,97 +556,49 @@ Chain.prototype.getLast = function getLast(cb) { }; Chain.prototype.getStartHeight = function getStartHeight() { - if (!this.options.fullNode) { - if (this.options.startHeight != null) { - return this.options.startHeight; - } - return 0; - } - return this.index.heights[this.index.heights.length - 1]; + if (this.options.startHeight != null) + return this.options.startHeight; + return 0; }; Chain.prototype.locatorHashes = function locatorHashes(start) { - var chain = this.index.hashes; - var hashes = []; - var top = chain.length - 1; - var step = 1; - var i; + assert(start == null); - if (typeof start === 'string') { - // Hash - for (i = top; i >= 0; i--) { - if (chain[i] === start) { - top = i; - break; - } - } - } else if (typeof start === 'number') { - // Height - start = this.index.heights.indexOf(start); - if (start !== -1) - top = start; - } + if (this.index.hashes.length === 1) + return [this.index.hashes[0]]; - i = top; - for (;;) { - hashes.push(chain[i]); - i = i - step; - if (i <= 0) { - hashes.push(chain[0]); - break; - } - if (hashes.length >= 10) - step *= 2; - } - - return hashes; + return [ + this.index.hashes[this.index.hashes.length - 1], + this.index.hashes[0] + ]; }; Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { - var self = this; - var root = hash; - - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - while (this.orphan.bmap[hash]) { - root = hash; - hash = this.orphan.bmap[hash].prevBlock; - } - - return root; + assert(false); }; Chain.prototype.getHeight = function getHeight(hash) { - var i, height; + var entry = this.byHash(hash); - if (Array.isArray(hash)) - hash = utils.toHex(hash); - else if (hash.hash) - hash = hash.hash('hex'); - - i = this.index.hashes.indexOf(hash); - height = this.index.heights[i]; - - if (height == null) + if (!entry) return -1; - return height; + return entry.height; }; Chain.prototype.getNextBlock = function getNextBlock(hash) { - var height, nextHeight, i; + var entry = this.byHeight(hash); + var nextHeight; - height = this.getHeight(hash); - nextHeight = this.index.heights[i + 1]; - i = this.index.heights.indexOf(nextHeight); - - if (nextHeight !== height + 1) + if (!entry) return null; - return this.index.hashes[i] || 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.toJSON = function toJSON() { @@ -708,4 +683,8 @@ Chain.prototype.fromJSON = function fromJSON(json) { this.index.bloom.add(this.index.hashes[i], 'hex'); }; +/** + * Expose + */ + module.exports = Chain; diff --git a/lib/bcoin/fullchain.js b/lib/bcoin/fullchain.js new file mode 100644 index 00000000..db1039ff --- /dev/null +++ b/lib/bcoin/fullchain.js @@ -0,0 +1,557 @@ +/** + * fullchain.js - fullnode 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 bn = require('bn.js'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; + +/** + * Chain + */ + +function Chain(options) { + 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.orphan = { + map: {}, + bmap: {}, + count: 0 + }; + + this.index = { + entries: [], + // Get hash by height + hashes: [], + // Get height by hash + heights: {}, + lastTs: 0 + }; + + this.fromJSON({ + v: 1, + type: 'chain', + network: network.type, + entries: [ + { + hash: utils.toHex(network.genesis._hash), + version: network.genesis.version, + // prevBlock: utils.toHex(network.genesis.prevBlock), + ts: network.genesis.ts, + bits: network.genesis.bits, + height: 0 + } + ] + }); + + // Last TS after preload, needed for fill percent + this.index.lastTs = this.index.entries[this.index.entries.length - 1].ts; + + bcoin.chain.global = this; + + this.loading = false; + this._init(); +} + +inherits(Chain, EventEmitter); + +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) { + data.value.hash = data.key.slice(self.prefix.length); + self._addIndex(ChainBlock.fromJSON(self, data.value)); + }); + + 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._addIndex = function _addIndex(entry) { + var self = this; + + if (this.index.entries[entry.hash]) + return new Error('Already added.'); + + if (this.index.hashes[entry.height] === entry.hash) + return new Error('Duplicate height.'); + + checkpoint = network.checkpoints[entry.height]; + if (checkpoint) { + this.emit('checkpoint', entry.height, entry.hash, checkpoint); + if (hash !== checkpoint) { + this.resetLastCheckpoint(entry.height); + this.emit('fork', entry.height, entry.hash, checkpoint); + return new Error('Forked chain at checkpoint.'); + } + } + + this.index.entries[entry.height] = entry; + this.index.hashes[entry.height] = entry.hash; + this.index.heights[entry.hash] = entry.height; + + this._save(entry); +}; + +Chain.prototype.resetLastCheckpoint = function resetLastCheckpoint(height) { + var lastHeight = Object.keys(network.checkpoints).sort().indexOf(height) - 1; + + if (lastHeight < 0) + i = 0; + + this.resetHeight(lastHeight); +}; + +Chain.prototype.resetHeight = function resetHeight(height) { + var self = this; + var forked = this.index.entries.slice(height + 1); + + this.orphan.map = {}; + this.orphan.bmap = {}; + this.orphan.count = 0; + this.index.entries.length = height + 1; + this.index.heights = this.index.entries.reduce(function(out, entry) { + out[entry.hash] = entry.height; + return out; + }, {}); + this.index.hashes.length = height + 1; + + this.index.lastTs = this.index.entries[this.index.entries.length - 1].ts; + + if (!this.storage) + return; + + forked.forEach(function(entry) { + self._del(entry); + }); +}; + +Chain.prototype.add = function add(block) { + if (this.loading) { + this.once('load', function() { + this.add(block); + }); + return; + } + + var res = false; + var err = null; + var initial = block; + var hash, prev, prevProbe, range, i, entry; + + for (;;) { + // No need to revalidate orphans + if (!res && !block.verify()) { + err = new Error('Block verification failed.'); + break; + } + + hash = block.hash('hex'); + prev = block.prevBlock; + + // If the block is already known to be an orphan + if (this.orphan.map[prev]) { + err = new Error('Block is a known orphan.'); + break; + } + + i = this.index.heights[prev]; + + // If previous block wasn't ever seen - add current to orphans + if (i == null) { + this.orphan.count++; + this.orphan.map[prev] = block; + this.orphan.bmap[hash] = block; + break; + } + + entry = new ChainBlock(this, { + hash: hash, + version: block.version, + // prevBlock: prev, + ts: block.ts, + bits: block.bits, + height: i + 1 + }); + + // Already have block + if (this.index.hashes[entry.height] === hash) + break; + + assert(this.index.heights[entry.hash] == null); + + // Remove forked nodes from storage, if shorter chain is detected + if (this.index.hashes[entry.height]) { + if (!this.tip) { + this.tip = entry; + } else if (this.tip.chainwork.cmp(entry.chainwork) < 0) { + // Found fork + this.resetHeight(entry.height - 1); + this.tip = entry; + } + } + + // Validated known block at this point - add it to index + this._addIndex(entry); + + // At least one block was added + res = true; + + 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--; + } + + return err; +}; + +Chain.prototype.has = function has(hash, noProbe, cb) { + var res; + + if (typeof noProbe === 'function') { + cb = noProbe; + noProbe = false; + } + + if (this.loading) { + this.once('load', function() { + this.has(hash, noProbe, cb); + }); + return; + } + + res = this.hasBlock(hash) || this.hasOrphan(hash); + + if (!cb) + return res; + + cb = utils.asyncify(cb); + return cb(res); +}; + +Chain.prototype.byHeight = function byHeight(height) { + if (this.loading) + return null; + + return this.index.entries[height]; +}; + +Chain.prototype.byHash = function byHash(hash) { + if (this.loading) + return null; + + if (Array.isArray(hash)) + hash = utils.toHex(hash); + else if (hash.hash) + hash = hash.hash('hex'); + + return this.byHeight(this.index.heights[hash]); +}; + +Chain.prototype.hasBlock = function hasBlock(hash) { + if (this.loading) + return false; + + return !!this.byHash(hash); +}; + +Chain.prototype.hasOrphan = function hasOrphan(hash) { + if (this.loading) + return false; + + if (Array.isArray(hash)) + hash = utils.toHex(hash); + else if (hash.hash) + hash = hash.hash('hex'); + + return !!this.orphan.bmap[hash]; +}; + +Chain.prototype.get = function get(hash, force, cb) { + assert(false); +}; + +Chain.prototype.isFull = function isFull() { + var last = this.index.entries[this.index.entries.length - 1].ts; + var delta = (+new Date() / 1000) - last; + return delta < 40 * 60; +}; + +Chain.prototype.fillPercent = function fillPercent() { + var total = (+new Date() / 1000 - 40 * 60) - this.index.lastTs; + var current = this.index.entries[this.index.entries.length - 1].ts - this.index.lastTs; + return Math.max(0, Math.min(current / total, 1)); +}; + +Chain.prototype.hashesInRange = function hashesInRange(start, end, cb) { + assert(false); +}; + +Chain.prototype.getLast = function getLast(cb) { + var res; + + if (this.loading) { + this.once('load', function() { + this.getLast(cb); + }); + return; + } + + res = this.index.hashes[this.index.hashes.length - 1]; + + if (!cb) + return res; + + cb = utils.asyncify(cb); + return cb(res); +}; + +Chain.prototype.getStartHeight = function getStartHeight() { + if (this.loading) + return 0; + + return this.index.entries[this.index.entries.length - 1].height; +}; + +Chain.prototype.locatorHashes = function locatorHashes(start) { + var chain = this.index.hashes; + var hashes = []; + var top = chain.length - 1; + var step = 1; + var i; + + if (start) { + if (Array.isArray(start)) + start = utils.toHex(start); + else if (start.hash) + start = start.hash('hex'); + } + + if (typeof start === 'string') { + // Hash + if (this.index.heights[start]) + top = this.index.heights[start]; + } else if (typeof start === 'number') { + // Height + top = start; + } + + i = top; + for (;;) { + hashes.push(chain[i]); + i = i - step; + if (i <= 0) { + hashes.push(chain[0]); + break; + } + if (hashes.length >= 10) + step *= 2; + } + + return hashes; +}; + +Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { + var self = this; + var root = hash; + + if (Array.isArray(hash)) + hash = utils.toHex(hash); + else if (hash.hash) + hash = hash.hash('hex'); + + while (this.orphan.bmap[hash]) { + root = hash; + hash = this.orphan.bmap[hash].prevBlock; + } + + return root; +}; + +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.byHash(hash); + + if (!entry || !entry.next) + return null; + + return entry.next.hash; +}; + +Chain.prototype._save = function(entry) { + var self = this; + + if (!this.storage) + return; + + this.storage.put(this.prefix + entry.hash, entry.toJSON(), function(err) { + if (err) + self.emit('error', err); + }); +}; + +Chain.prototype._del = function(entry) { + var self = this; + + if (!this.storage) + return; + + this.storage.del(this.prefix + entry.hash, function(err) { + if (err) + self.emit('error', err); + }); +}; + +Chain.prototype.toJSON = function toJSON() { + var entries = this.index.entries; + + return { + v: 1, + type: 'chain', + network: network.type, + entries: entries.map(function(entry) { + return entry.toJSON(); + }) + }; +}; + +Chain.prototype.fromJSON = function fromJSON(json) { + assert.equal(json.v, 1); + assert.equal(json.type, 'chain'); + assert.equal(json.network, network.type); + + json.entries.forEach(function(entry) { + this._addIndex(ChainBlock.fromJSON(this, entry)); + }, this); + + if (this.index.entries.length === 0) + this.add(new bcoin.block(network.genesis, 'block')); +}; + +/** + * ChainBlock + */ + +function ChainBlock(chain, data) { + this.chain = chain; + this.hash = data.hash; + this.version = data.version; + // this.prevBlock = data.prevBlock; + this.ts = data.ts; + this.bits = data.bits; + this.height = data.height; + this.chainwork = this.getChainwork(); +} + +ChainBlock.prototype.__defineGetter__('prev', function() { + return this.chain.index.entries[this.height - 1]; +}); + +ChainBlock.prototype.__defineGetter__('next', function() { + return this.chain.index.entries[this.height + 1]; +}); + +ChainBlock.prototype.__defineGetter__('proof', function() { + var target = utils.fromCompact(this.bits); + if (target.isNeg() || target.cmpn(0) === 0) + return new bn(0); + // May be faster: + // return new bn(1).shln(256).div(target.addn(1)); + return new bn(2).pow(new bn(256)).div(target.addn(1)); +}); + +ChainBlock.prototype.getChainwork = function() { + if (!this.prev) + return new bn(0); + + return (this.prev ? this.prev.chainwork : new bn(0)).add(this.proof); +}; + +ChainBlock.prototype.toJSON = function() { + // return [ + // this.hash, + // this.version, + // // this.prevBlock, + // this.ts, + // this.bits, + // this.height + // }; + return { + hash: this.hash, + version: this.version, + // prevBlock: this.prevBlock, + ts: this.ts, + bits: this.bits, + height: this.height + }; +}; + +ChainBlock.fromJSON = function(chain, json) { + // return new ChainBlock(chain, { + // hash: json[0], + // version: json[1], + // ts: json[2], + // bits: json[3], + // height: json[4] + // }); + return new ChainBlock(chain, json); +}; + +/** + * Expose + */ + +module.exports = Chain; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index d7a9fd1c..105e0cc8 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -19,6 +19,7 @@ var network = bcoin.protocol.network; function Pool(options) { var self = this; + var Chain; if (!(this instanceof Pool)) return new Pool(options); @@ -57,14 +58,22 @@ function Pool(options) { hiReached: false }; + if (this.options.fullNode) { + if (!options.loadTimeout) + this.load.timeout = 30000; + if (!options.loadInterval) + this.load.interval = 5000; + } + this.maxRetries = options.maxRetries || 42; this.requestTimeout = options.requestTimeout || 10000; - this.chain = new bcoin.chain({ + Chain = this.options.fullNode + ? bcoin.fullChain + : bcoin.spvChain; + + this.chain = new Chain({ storage: this.storage, - // Since regular blocks contain transactions and full merkle - // trees, it's risky to cache 2000 blocks. Let's do 100. - cacheLimit: this.options.fullNode ? 100 : null, fullNode: this.options.fullNode, startHeight: this.options.startHeight }); @@ -77,11 +86,6 @@ function Pool(options) { (Math.random() * 0xffffffff) | 0 ); - this.bestHeight = 0; - this.bestBlock = null; - this.needSync = true; - this.syncPeer = null; - this.peers = { // Peers that are loading blocks themselves block: [], @@ -92,7 +96,9 @@ function Pool(options) { }; this.block = { - lastHash: null + lastHash: null, + bestHeight: 0, + bestHash: null }; this.request = { @@ -153,19 +159,23 @@ Pool.prototype._init = function _init() { this._load(); this.chain.on('missing', function(hash, preload, parent) { - if (self.options.fullNode) return; - self._request('block', hash, { force: true }); - self._scheduleRequests(); - self._loadRange(preload); + if (!self.options.fullNode) { + self._request('filtered', hash, { force: true }); + self._scheduleRequests(); + self._loadRange(preload); + } }); this.chain.on('fork', function(height, hash, checkpoint) { - var peer = self.syncPeer; + var peer = self.peers.load; + + if (!self.options.fullNode) + return; + if (!peer) return; - delete self.syncPeer; + peer.destroy(); - self.startSync(); }); this.options.wallets.forEach(function(w) { @@ -180,7 +190,7 @@ Pool.prototype._addLoader = function _addLoader() { if (this.destroyed) return; - if (this.peers.load !== null) + if (this.peers.load != null) return; peer = new bcoin.peer(this, this.createSocket, { @@ -188,6 +198,7 @@ Pool.prototype._addLoader = function _addLoader() { startHeight: this.options.startHeight, relay: this.options.relay }); + this.peers.load = peer; peer.on('error', function(err) { @@ -204,18 +215,23 @@ Pool.prototype._addLoader = function _addLoader() { self._addLoader(); } - interval = setInterval(function() { - self._load(); - }, this.load.interval); - peer.once('ack', function() { peer.updateWatch(); if (!self._load()) clearTimeout(timer); }); - if (this.options.fullNode) - return; + peer.on('version', function(version) { + if (version.height > self.block.bestHeight) + self.block.bestHeight = version.height; + self.emit('version', version, peer); + }); + + function load() { + self._load(); + } + + interval = setInterval(load, this.load.interval); function destroy() { // Chain is full and up-to-date @@ -231,44 +247,119 @@ Pool.prototype._addLoader = function _addLoader() { timer = setTimeout(destroy, this.load.timeout); + peer.on('block', function(block) { + self._handleBlock(block, peer); + }); + // Split blocks and request them using multiple peers peer.on('blocks', function(hashes) { + var i, hash; + + self.emit('blocks', hashes); + if (hashes.length === 0) { // Reset global load self.block.lastHash = null; return; } - // Request each block - hashes.forEach(function(hash) { - self._request('block', hash); - }); + if (!self.options.fullNode) { + // Request each block + hashes.forEach(function(hash) { + self._request('filtered', hash); + }); + + self._scheduleRequests(); + + // The part of the response is in chain, no need to escalate requests + async.every(hashes, function(hash, cb) { + self.chain.has(utils.toHex(hash), function(res) { + cb(!res); + }); + }, function(allNew) { + if (!allNew) { + self.block.lastHash = null; + return; + } + + // Store last hash to continue global load + self.block.lastHash = hashes[hashes.length - 1]; + + clearTimeout(timer); + + // Reinstantiate timeout + if (self._load()) + timer = setTimeout(destroy, self.load.timeout); + }); + + return; + } + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + if (self.chain.hasOrphan(hash)) { + peer.loadBlocks(self.chain.locatorHashes(), self.chain.getOrphanRoot(hash)); + continue; + } + + if (i === hashes.length - 1) + peer.loadBlocks(self.chain.locatorHashes(), 0); + + if (!self.chain.hasBlock(hash)) + self._request('block', hash); + } self._scheduleRequests(); - // The part of the response is in chain, no need to escalate requests - async.every(hashes, function(hash, cb) { - self.chain.has(utils.toHex(hash), function(res) { - cb(!res); - }); - }, function(allNew) { - if (!allNew) { - self.block.lastHash = null; - return; - } + self.block.lastHash = hashes[hashes.length - 1]; - // Store last hash to continue global load - self.block.lastHash = hashes[hashes.length - 1]; + clearInterval(interval); + interval = setInterval(load, self.load.interval); - clearTimeout(timer); - - // Reinstantiate timeout - if (self._load()) - timer = setTimeout(destroy, self.load.timeout); - }); + clearTimeout(timer); + timer = setTimeout(destroy, self.load.timeout); }); }; +Pool.prototype._handleMerkle = function _handleMerkle(block, peer) { + this._response(block); + this.chain.add(block); + this.emit('chain-progress', this.chain.fillPercent(), peer); + this.emit('block', block, peer); +}; + +Pool.prototype._handleBlock = function _handleBlock(block, peer) { + var chainIndex = this.chain.index; + var hash, len, orphan, err; + + backoff = 0; + + this._response(block); + + hash = block.hash('hex'); + len = chainIndex.hashes.length; + orphan = this.chain.hasOrphan(block); + + err = this.chain.add(block); + if (err) + this.emit('chain-error', err, peer); + + this.emit('_block', block, peer); + + if (this.chain.hasOrphan(block)) { + peer.loadBlocks(this.chain.locatorHashes(), this.chain.getOrphanRoot(block)); + if (!orphan) + this.emit('orphan', block, peer); + return; + } + + if (chainIndex.hashes.length === len) + return; + + this.emit('chain-progress', this.chain.fillPercent(), peer); + this.emit('block', block, peer); +}; + Pool.prototype.isFull = function isFull() { return this.chain.isFull(); }; @@ -304,9 +395,7 @@ Pool.prototype._loadRange = function _loadRange(hashes, force) { Pool.prototype._load = function _load() { var self = this; - - if (this.options.fullNode) - return; + var next; if (this.request.queue.length >= this.load.hwm) { this.load.hiReached = true; @@ -315,19 +404,32 @@ Pool.prototype._load = function _load() { this.load.hiReached = false; - // Load more blocks, starting from last hash - if (this.block.lastHash) - next(this.block.lastHash); - else - this.chain.getLast(next); + console.log( + 'Requesting inv packet from %s with getblocks', + this.peers.load.address); - function next(hash) { - if (!self.peers.load) - self._addLoader(); + if (!this.options.fullNode) { + next = function(hash) { + if (!self.peers.load) + self._addLoader(); + else + self.peers.load.loadBlocks([ hash ]); + }; + + // Load more blocks, starting from last hash + if (this.block.lastHash) + next(this.block.lastHash); else - self.peers.load.loadBlocks([ hash ]); + this.chain.getLast(next); + + return true; } + if (!this.peers.load) + this._addLoader(); + else + this.peers.load.loadBlocks(this.chain.locatorHashes(), 0); + return true; }; @@ -392,48 +494,13 @@ Pool.prototype._addPeer = function _addPeer(backoff) { peer.on('merkleblock', function(block) { // Reset backoff, peer seems to be responsive backoff = 0; - - self._response(block); - self.chain.add(block); - self.emit('chain-progress', self.chain.fillPercent(), peer); - self.emit('block', block, peer); + self._handleMerkle(block, peer); }); } else { peer.on('block', function(block) { - var hashes = self.chain.index.hashes; - var hash, len, orphan, err; - - if (self.syncPeer !== peer) - return; - + // Reset backoff, peer seems to be responsive backoff = 0; - - self._response(block); - - hash = block.hash('hex'); - len = hashes.length; - orphan = self.chain.hasOrphan(block); - - err = self.chain.add(block); - if (err) - self.emit('chain-error', err, peer); - - self.emit('_block', block, peer); - - if (self.chain.hasOrphan(block)) { - peer.loadBlocks(self.chain.locatorHashes(), self.chain.getOrphanRoot(block)); - if (!orphan) - self.emit('orphan', block, peer); - return; - } - - if (hashes.length === len) - return; - - self.needSync = hashes[hashes.length - 1] !== self.bestBlock; - - self.emit('chain-progress', self.chain.fillPercent(), peer); - self.emit('block', block, peer); + self._handleBlock(block, peer); }); } @@ -461,7 +528,7 @@ Pool.prototype._addPeer = function _addPeer(backoff) { peer.on('blocks', function(blocks) { if (blocks.length === 1) - self.bestBlock = peer.bestBlock; + self.block.bestHash = peer.bestBlock; self.emit('blocks', blocks, peer); }); @@ -470,80 +537,26 @@ Pool.prototype._addPeer = function _addPeer(backoff) { }); peer.on('version', function(version) { - if (version.height > self.bestHeight) - self.bestHeight = version.height; + if (version.height > self.block.bestHeight) + self.block.bestHeight = version.height; self.emit('version', version, peer); }); - peer.on('ack', function() { - if (self.options.fullNode) { - if (self.peers.block.length >= Math.min(5, self.size)) - self.startSync(); - } - }); - utils.nextTick(function() { self.emit('peer', peer); }); }; Pool.prototype.bestPeer = function bestPeer() { - var best = null; - - this.peers.block.forEach(function(peer) { + return this.peers.block.reduce(function(best, peer) { if (!peer.version || !peer.socket) return; if (!best || peer.version.height > best.version.height) - best = peer; - }); + return peer; - if (best) - this.emit('debug', 'Best peer: %s', best.address); - - return best; -}; - -Pool.prototype.startSync = function startSync(peer) { - if (!this.options.fullNode) - return; - - if (this.syncPeer) - return; - - peer = peer || this.bestPeer(); - if (!peer) - return; - - this.syncPeer = peer; - - peer.on('blocks', function(hashes) { - var req = []; - var i, hash; - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - if (peer.chain.hasOrphan(hash)) { - peer.loadBlocks(peer.chain.locatorHashes(), peer.chain.getOrphanRoot(hash)); - continue; - } - if (!peer.chain.hasBlock(hash)) { - req.push({ type: 'block', hash: hash }); - continue; - } - if (i === hashes.length - 1) { - peer.loadBlocks(peer.chain.locatorHashes(), 0); - continue; - } - } - - if (req.length) - peer.getData(req); - }); - - this.emit('debug', 'version (%s): sending locator hashes', peer.address); - - peer.loadBlocks(this.chain.locatorHashes(), 0); + return best; + }, null); }; Pool.prototype._removePeer = function _removePeer(peer) { @@ -557,16 +570,14 @@ Pool.prototype._removePeer = function _removePeer(peer) { if (this.peers.load === peer) this.peers.load = null; - - if (this.syncPeer === peer) { - delete this.syncPeer; - this.startSync(); - } }; Pool.prototype.watch = function watch(id) { var hid, i; + if (this.options.fullNode) + return; + if (id instanceof bcoin.wallet) { this.watchWallet(id); return; @@ -595,6 +606,9 @@ Pool.prototype.watch = function watch(id) { Pool.prototype.unwatch = function unwatch(id) { var i; + if (this.options.fullNode) + return; + id = utils.toHex(id); if (!this.bloom.test(id, 'hex')) @@ -648,6 +662,9 @@ Pool.prototype.addWallet = function addWallet(w, defaultTs) { if (!ts) ts = defaultTs || ((+new Date / 1000) - 7 * 24 * 3600); + if (self.options.fullNode) + return; + self.search(false, ts, e); } @@ -663,12 +680,16 @@ Pool.prototype.removeWallet = function removeWallet(w) { }; Pool.prototype.watchWallet = function watchWallet(w) { + if (this.options.fullNode) + return; + if (w.type === 'scripthash') { // For the redeem script hash in outputs: this.watch(w.getFullHash()); // For the redeem script in inputs: this.watch(w.getFullPublicKey()); } + // For the pubkey hash in outputs: this.watch(w.getOwnHash()); // For the pubkey in inputs: @@ -676,12 +697,16 @@ Pool.prototype.watchWallet = function watchWallet(w) { }; Pool.prototype.unwatchWallet = function unwatchWallet(w) { + if (this.options.fullNode) + return; + if (w.type === 'scripthash') { // For the redeem script hash in p2sh outputs: this.unwatch(w.getFullHash()); // For the redeem script in p2sh inputs: this.unwatch(w.getFullPublicKey()); } + // For the pubkey hash in p2pk/multisig outputs: this.unwatch(w.getOwnHash()); // For the pubkey in p2pkh inputs: @@ -691,6 +716,9 @@ Pool.prototype.unwatchWallet = function unwatchWallet(w) { Pool.prototype.search = function search(id, range, e) { var self = this; + if (this.options.fullNode) + return; + e = e || new EventEmitter(); // Optional id argument @@ -726,6 +754,8 @@ Pool.prototype.search = function search(id, range, e) { self.watch(id); self._loadRange(hashes, true); + // XXX + // return; hashes = hashes.slice().reverse(); hashes.forEach(function(hash, i) { // Get the block that is in index @@ -802,7 +832,7 @@ Pool.prototype._request = function _request(type, hash, options, cb) { } // Block should be not in chain, or be requested - if (!options.force && type === 'block') + if (!options.force && (type === 'block' || type === 'filtered')) return this.chain.has(hash, true, next); return next(false); @@ -846,6 +876,7 @@ Pool.prototype._scheduleRequests = function _scheduleRequests() { Pool.prototype._doRequests = function _doRequests() { var queue, above, items, below; var red, count, split, i, off, req, j; + var mapReq; if (this.request.active >= this.parallel) return; @@ -864,19 +895,29 @@ Pool.prototype._doRequests = function _doRequests() { if (above && below && this.load.hiReached) this._load(); - function mapReq(item) { - return item.start(this.peers.block[i]); + if (!this.options.fullNode) { + mapReq = function(item) { + return item.start(this.peers.block[i]); + }; + + // Split list between peers + red = this.redundancy; + count = this.peers.block.length; + split = Math.ceil(items.length * red / count); + for (i = 0, off = 0; i < count; i += red, off += split) { + req = items.slice(off, off + split).map(mapReq, this); + for (j = 0; j < red && i + j < count; j++) + this.peers.block[i + j].getData(req); + } + + return; } - // Split list between peers - red = this.redundancy; - count = this.peers.block.length; - split = Math.ceil(items.length * red / count); - for (i = 0, off = 0; i < count; i += red, off += split) { - req = items.slice(off, off + split).map(mapReq, this); - for (j = 0; j < red && i + j < count; j++) - this.peers.block[i + j].getData(req); - } + req = items.map(function(item) { + return item.start(this.peers.load); + }, this); + + this.peers.load.getData(req); }; Pool.prototype.getTX = function getTX(hash, range, cb) { @@ -1056,13 +1097,8 @@ LoadRequest.prototype.start = function start(peer) { this.peer = peer; this.peer.once('close', this.onclose); - if (this.type === 'block') - reqType = 'filtered'; - else if (this.type === 'tx') - reqType = 'tx'; - return { - type: reqType, + type: this.type, hash: this.hash }; }; diff --git a/scripts/update.js b/scripts/update.js index 84d3c18e..b4afd650 100644 --- a/scripts/update.js +++ b/scripts/update.js @@ -5,14 +5,7 @@ var net = require('net'); var path = require('path'); var bcoin = require('../'); -var addrs = [ - 'seed.bitcoin.sipa.be', - 'dnsseed.bluematt.me', - 'dnsseed.bitcoin.dashjr.org', - 'seed.bitcoinstats.com', - 'seed.bitnodes.io', - 'bitseed.xf2.org' -]; +var addrs = bcoin.protocol.network.seeds.slice(); var pool = bcoin.pool({ size: 32, @@ -25,6 +18,8 @@ var pool = bcoin.pool({ } }); +pool.on('error', function() {}); + console.log('Updating bcoin preloaded chain...'); pool.on('block', function(block) {