diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 7499df81..11a94914 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -1685,15 +1685,17 @@ Chain.prototype.isInitial = function isInitial(force) { if (!force && this.synced) return false; - if (this.height < this.network.checkpoints.lastHeight) + if (this.options.useCheckpoints) { + if (this.height < this.network.lastCheckpoint) + return true; + } + + if (this.tip.ts < util.now() - this.network.block.maxTipAge) return true; if (this.tip.chainwork.cmp(this.network.pow.chainwork) < 0) return true; - if (this.tip.ts < util.now() - this.network.block.maxTipAge) - return true; - return false; }; diff --git a/lib/blockchain/chainentry.js b/lib/blockchain/chainentry.js index fd5810bf..84890f01 100644 --- a/lib/blockchain/chainentry.js +++ b/lib/blockchain/chainentry.js @@ -354,7 +354,7 @@ ChainEntry.prototype.getMedianTimeAsync = co(function* getMedianTimeAsync() { ChainEntry.prototype.isHistorical = function isHistorical() { if (this.chain.options.useCheckpoints) { - if (this.height + 1 <= this.chain.network.checkpoints.lastHeight) + if (this.height + 1 <= this.chain.network.lastCheckpoint) return true; } return false; diff --git a/lib/net/peer.js b/lib/net/peer.js index b3c4afc9..85ee00e7 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -616,12 +616,6 @@ Peer.prototype.finalize = co(function* finalize() { this.send(new packets.AddrPacket([this.pool.address])); } - // Ask for headers-only. - if (this.options.headers) { - if (this.version >= common.SENDHEADERS_VERSION) - this.send(new packets.SendHeadersPacket()); - } - // We want compact blocks! if (this.options.compact) { if (this.version >= common.COMPACT_VERSION) @@ -3075,7 +3069,7 @@ Peer.prototype.getBlocks = co(function* getBlocks(tip, stop) { */ Peer.prototype.sync = co(function* sync() { - var tip; + var tip, checkpoint; if (!this.pool.syncing) return; @@ -3107,11 +3101,11 @@ Peer.prototype.sync = co(function* sync() { this.lastBlock = util.ms(); - if (this.options.headers) { - if (!this.chain.tip.isGenesis()) - tip = this.chain.tip.prevBlock; - - return yield this.getHeaders(tip); + if (this.pool.headersFirst) { + tip = this.chain.tip; + checkpoint = this.pool.nextCheckpoint; + this.sendGetHeaders([tip.hash], checkpoint.hash); + return; } return yield this.getBlocks(); diff --git a/lib/net/pool.js b/lib/net/pool.js index 84ed4d31..e3893a2b 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -32,7 +32,6 @@ var InvItem = require('../primitives/invitem'); var Map = require('../utils/map'); var invTypes = InvItem.types; var VerifyError = errors.VerifyError; -var VerifyResult = errors.VerifyResult; /** * A pool of peers for handling all network activity. @@ -113,6 +112,12 @@ function Pool(options) { this.pendingWatch = null; this.pendingRefill = null; + this.headersFirst = false; + this.headerChain = new List(); + this.headerNext = null; + this.nextCheckpoint = null; + this.checkpoints = []; + this.peers = new PeerList(); this.authdb = new BIP150.AuthDB(this.options); this.address = new NetAddress(this.options); @@ -138,6 +143,18 @@ util.inherits(Pool, AsyncObject); Pool.prototype._init = function _init() { var self = this; + var keys = Object.keys(this.network.checkpoints); + var i, key, hash, height; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + hash = this.network.checkpoints[key]; + height = +key; + + this.checkpoints.push(new BlockNode(hash, height)); + } + + this.checkpoints.sort(sortNodes); this.server.on('error', function(err) { self.emit('error', err); @@ -232,8 +249,42 @@ Pool.prototype._open = co(function* _open() { this.logger.info('Identity public key: %s.', key.toString('hex')); this.logger.info('Identity address: %s.', BIP150.address(key)); } + + this.resetChain(); }); +/** + * Reset header chain. + */ + +Pool.prototype.resetChain = function resetChain() { + var tip = this.chain.tip; + var checkpoint; + + if (!this.options.headers) + return; + + if (!this.options.useCheckpoints) + return; + + this.headersFirst = false; + this.nextCheckpoint = null; + this.headerChain.reset(); + this.headerNext = null; + + checkpoint = this.getNextCheckpoint(tip.height); + + if (checkpoint) { + this.headersFirst = true; + this.nextCheckpoint = checkpoint; + this.headerChain.push(new BlockNode(tip.hash, tip.height)); + this.logger.info( + 'Initialized header chain to height %d (checkpoint=%d).', + tip.height, + checkpoint.height); + } +}; + /** * Close and destroy the pool. * @alias Pool#close @@ -287,6 +338,7 @@ Pool.prototype._connect = co(function* connect() { } yield this.authdb.discover(); + yield this.hosts.discover(); if (this.hosts.size() === 0) @@ -348,6 +400,11 @@ Pool.prototype._disconnect = co(function* disconnect() { this.pendingRefill = null; } + this.headersFirst = false; + this.nextCheckpoint = null; + this.headerChain.reset(); + this.headerNext = null; + yield this.unlisten(); this.syncing = false; @@ -708,8 +765,10 @@ Pool.prototype.handleClose = co(function* handleClose(peer, connected) { this.removePeer(peer); - if (loader) + if (loader) { this.logger.info('Removed loader peer (%s).', peer.hostname); + this.resetChain(); + } if (!this.loaded) return; @@ -800,7 +859,8 @@ Pool.prototype.handleBlock = co(function* handleBlock(peer, block) { Pool.prototype._handleBlock = co(function* handleBlock(peer, block) { var hash = block.hash('hex'); - var requested; + var isCheckpoint = false; + var requested, node, checkpoint; if (!this.syncing) return; @@ -817,6 +877,22 @@ Pool.prototype._handleBlock = co(function* handleBlock(peer, block) { return; } + if (this.headersFirst) { + node = this.headerChain.head; + assert(node); + + if (hash === node.hash) { + if (hash === this.nextCheckpoint.hash) { + this.logger.info( + 'Received checkpoint block %s (%d).', + hash, node.height); + isCheckpoint = true; + } else { + this.headerChain.shift(); + } + } + } + try { yield this.chain.add(block); } catch (err) { @@ -824,7 +900,7 @@ Pool.prototype._handleBlock = co(function* handleBlock(peer, block) { throw err; if (err.reason === 'bad-prevblk') { - if (this.options.headers) { + if (this.headersFirst) { peer.increaseBan(10); throw err; } @@ -862,6 +938,30 @@ Pool.prototype._handleBlock = co(function* handleBlock(peer, block) { this.chain.height, block.rhash()); } + + if (!this.headersFirst) + return; + + if (!isCheckpoint) { + yield this.resolveHeaders(peer); + return; + } + + node = this.nextCheckpoint; + checkpoint = this.getNextCheckpoint(node.height); + + if (checkpoint) { + this.nextCheckpoint = checkpoint; + peer.sendGetHeaders([node.hash], checkpoint.hash); + return; + } + + this.headersFirst = false; + this.nextCheckpoint = null; + this.headerChain.reset(); + this.headerNext = null; + + peer.sendGetBlocks([hash], null); }); /** @@ -964,16 +1064,18 @@ Pool.prototype.handleHeaders = co(function* handleHeaders(peer, headers) { */ Pool.prototype._handleHeaders = co(function* handleHeaders(peer, headers) { - var items = []; - var ret = new VerifyResult(); - var i, header, hash, last; + var isCheckpoint = false; + var i, header, hash, height, last, node; - if (!this.options.headers) + if (!this.headersFirst) return; if (!this.syncing) return; + if (!peer.isLoader()) + return; + this.logger.debug( 'Received %s headers from peer (%s).', headers.length, @@ -987,39 +1089,96 @@ Pool.prototype._handleHeaders = co(function* handleHeaders(peer, headers) { for (i = 0; i < headers.length; i++) { header = headers[i]; hash = header.hash('hex'); + last = this.headerChain.tail; - if (last && header.prevBlock !== last) { + assert(last); + + height = last.height + 1; + + if (header.prevBlock !== last.hash) { peer.increaseBan(100); throw new Error('Bad header chain.'); } - if (!header.verify(ret)) { - peer.reject(header, 'invalid', ret.reason, 100); + if (!header.verify()) { + peer.increaseBan(100); throw new Error('Invalid header.'); } - last = hash; + node = new BlockNode(hash, last.height + 1); - if (yield this.hasBlock(hash)) - continue; + if (!this.headerNext) + this.headerNext = node; - items.push(hash); + if (node.height === this.nextCheckpoint.height) { + if (node.hash !== this.nextCheckpoint.hash) { + peer.increaseBan(100); + throw new Error('Bad checkpoint header.'); + } + isCheckpoint = true; + } + + this.headerChain.push(node); } // Request the hashes we just added. - this.getBlock(peer, items); + if (isCheckpoint) { + this.headerChain.shift(); + yield this.resolveHeaders(peer); + return; + } - // Restart the getheaders process - // Technically `last` is not indexed yet so - // the locator hashes will not be entirely - // accurate. However, it shouldn't matter - // that much since FindForkInGlobalIndex - // simply tries to find the latest block in - // the peer's chain. - if (headers.length === 2000) - yield peer.getHeaders(last); + // Restart the getheaders process. + peer.sendGetHeaders([node.hash], this.nextCheckpoint.hash); }); +/** + * Request current header chain blocks. + * @param {Peer} peer + * @returns {Promise} + */ + +Pool.prototype.resolveHeaders = co(function* resolveHeaders(peer) { + var items = []; + var node; + + if (!this.headerNext) + return; + + for (node = this.headerNext; node; node = node.next) { + this.headerNext = node.next; + + if (yield this.hasBlock(node.hash)) + continue; + + items.push(node.hash); + } + + // Checkpoints should never be + // spaced more than 50k blocks + // apart from each other. + assert(items.length <= 50000); + + this.getBlock(peer, items); +}); + +/** + * Find the next highest checkpoint. + * @private + * @param {Number} height + * @returns {Object} + */ + +Pool.prototype.getNextCheckpoint = function getNextCheckpoint(height) { + var i, next; + + for (i = 0; i < this.checkpoints.length; i++) { + next = this.checkpoints[i]; + if (next.height > height) + return next; + } +}; + /** * Handle `inv` packet from peer (containing only BLOCK types). * Potentially request headers if headers mode is enabled. @@ -1061,14 +1220,8 @@ Pool.prototype._handleBlockInv = co(function* handleBlockInv(peer, hashes) { return; // Request headers instead. - if (this.options.headers) { - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - yield peer.getHeaders(null, hash); - } - + if (this.headersFirst) return; - } this.logger.debug( 'Received %s block hashes from peer (%s).', @@ -1908,8 +2061,6 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { if (options.noRelay != null) { assert(typeof options.noRelay === 'boolean'); this.noRelay = options.noRelay; - } else { - this.noRelay = this.spv === true; } if (options.host != null) { @@ -2040,6 +2191,8 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { if (this.spv) { this.requiredServices |= common.services.BLOOM; this.services &= ~common.services.NETWORK; + this.useCheckpoints = true; + this.noRelay = true; } if (this.selfish) @@ -2394,6 +2547,21 @@ BroadcastItem.prototype.inspect = function inspect() { + '>'; }; +/* + * Helpers + */ + +function BlockNode(hash, height) { + this.hash = hash; + this.height = height; + this.prev = null; + this.next = null; +} + +function sortNodes(a, b) { + return a.height - b.height; +} + /* * Expose */ diff --git a/lib/protocol/network.js b/lib/protocol/network.js index 02e150d0..bdc8f410 100644 --- a/lib/protocol/network.js +++ b/lib/protocol/network.js @@ -32,6 +32,7 @@ function Network(options) { this.port = options.port; this.alertKey = options.alertKey; this.checkpoints = options.checkpoints; + this.lastCheckpoint = options.lastCheckpoint; this.halvingInterval = options.halvingInterval; this.genesis = options.genesis; this.genesisBlock = options.genesisBlock; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index fc5b4ba4..0f117ec5 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -114,7 +114,13 @@ main.checkpoints = { 420000: 'a1ff746b2d42b834cb7d6b8981b09c265c2cabc016e8cc020000000000000000' }; -main.checkpoints.lastHeight = 420000; +/** + * Last checkpoint height. + * @const {Number} + * @default + */ + +main.lastCheckpoint = 420000; /** * @const {Number} @@ -528,7 +534,7 @@ testnet.checkpoints = { 900000: '9bd8ac418beeb1a2cf5d68c8b5c6ebaa947a5b766e5524898d6f350000000000' }; -testnet.checkpoints.lastHeight = 900000; +testnet.lastCheckpoint = 900000; testnet.halvingInterval = 210000; @@ -690,7 +696,7 @@ regtest.alertKey = new Buffer( 'hex'); regtest.checkpoints = {}; -regtest.checkpoints.lastHeight = 0; +regtest.lastCheckpoint = 0; regtest.halvingInterval = 150; @@ -845,7 +851,7 @@ segnet4.alertKey = new Buffer( 'hex'); segnet4.checkpoints = {}; -segnet4.checkpoints.lastHeight = 0; +segnet4.lastCheckpoint = 0; segnet4.halvingInterval = 210000; @@ -997,7 +1003,7 @@ simnet.alertKey = new Buffer('' simnet.checkpoints = {}; -simnet.checkpoints.lastHeight = 0; +simnet.lastCheckpoint = 0; simnet.halvingInterval = 210000; diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 5cd6e452..59db1f97 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1936,7 +1936,7 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { yield this.syncState(tip); if (this.options.useCheckpoints) { - if (tip.height <= this.network.checkpoints.lastHeight) + if (tip.height <= this.network.lastCheckpoint) return total; }