diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 9cd0a312f..408e1f01b 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -64,6 +64,8 @@ class Chain extends AsyncEmitter { this.orphanMap = new BufferMap(); this.orphanPrev = new BufferMap(); + + this.getPrunedMap = new BufferMap(); } /** @@ -1368,7 +1370,15 @@ class Chain extends AsyncEmitter { } // Do we already have this block? - if (await this.hasEntry(hash)) { + const existingEntry = await this.getEntry(hash); + + if (existingEntry && this.getPrunedMap.has(hash)) { + block = block.toBlock(); + this.emit('getblockpeer', existingEntry, block); + return null; + } + + if (existingEntry) { this.logger.debug('Already have block: %h.', block.hash()); throw new VerifyError(block, 'duplicate', 'duplicate', 0); } @@ -1925,6 +1935,21 @@ class Chain extends AsyncEmitter { return this.db.getBlock(hash); } + async getBlockPeer(hash) { + this.logger.warning('Block not found, attempting to download'); + + // Ensure hash not height + hash = await this.db.getHash(hash); + + const wait = new Promise((resolve, reject) => { + this.getPrunedMap.set(hash, resolve); + }); + + await this.emitAsync('getprunedblock', hash); + await wait; + return; + } + /** * Retrieve a block from the database (not filled with coins). * @param {Hash} block diff --git a/lib/client/node.js b/lib/client/node.js index 50800cac1..693e69222 100644 --- a/lib/client/node.js +++ b/lib/client/node.js @@ -164,9 +164,8 @@ class NodeClient extends Client { * @returns {Promise} */ - getFilter(filter) { - assert(typeof filter === 'string' || typeof filter === 'number'); - return this.get(`/filter/${filter}`); + getBlockPeer(hash) { + return this.call('get block peer', hash); } /** diff --git a/lib/net/pool.js b/lib/net/pool.js index 234b23bc2..a8f2a7a53 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -141,6 +141,16 @@ class Pool extends EventEmitter { this.handleBadOrphan('block', err, id); }); + this.chain.on('getprunedblock', async (hash) => { + // Find the first peer with a completed handshake + for (let peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.handshake) + continue; + + await this.getBlock(peer, [hash]); + } + }); + if (this.mempool) { this.mempool.on('tx', (tx) => { this.emit('tx', tx); @@ -2293,7 +2303,7 @@ class Pool extends EventEmitter { const hash = block.hash(); - if (!this.resolveBlock(peer, hash)) { + if (!this.options.neutrino && !this.resolveBlock(peer, hash)) { this.logger.warning( 'Received unrequested block: %h (%s).', block.hash(), peer.hostname()); @@ -2316,6 +2326,14 @@ class Pool extends EventEmitter { } // Block was orphaned. + + const resolve = this.chain.getPrunedMap.get(hash); + if (resolve) { + this.logger.warning('Received pruned block by special request'); + this.chain.getPrunedMap.delete(hash); + resolve(); + } + if (!entry) { if (this.checkpoints) { this.logger.warning( @@ -3690,6 +3708,7 @@ class PoolOptions { this.prefix = null; this.checkpoints = true; this.spv = false; + this.neutrino = false; this.bip37 = false; this.bip157 = false; this.listen = false; @@ -3772,12 +3791,17 @@ class PoolOptions { if (options.spv != null) { assert(typeof options.spv === 'boolean'); - assert(options.spv === this.chain.options.spv); this.spv = options.spv; } else { this.spv = this.chain.options.spv; } + if (options.neutrino != null) { + assert(options.compact !== true); + assert(typeof options.neutrino === 'boolean'); + this.neutrino = options.neutrino; + } + if (options.bip37 != null) { assert(typeof options.bip37 === 'boolean'); this.bip37 = options.bip37; @@ -3953,6 +3977,12 @@ class PoolOptions { this.listen = false; } + if (this.neutrino) { + this.requiredServices |= common.services.NODE_COMPACT_FILTERS; + this.checkpoints = true; + this.compact = false; + } + if (this.selfish) { this.services &= ~common.services.NETWORK; this.bip37 = false; diff --git a/lib/node/http.js b/lib/node/http.js index 8448ec015..c8e8316fb 100644 --- a/lib/node/http.js +++ b/lib/node/http.js @@ -498,6 +498,12 @@ class HTTP extends Server { return null; }); + socket.hook('get block peer', (...args) => { + const valid = new Validator(args); + const hash = valid.hash(0); + return this.chain.getBlockPeer(hash); + }); + socket.hook('estimate fee', (...args) => { const valid = new Validator(args); const blocks = valid.u32(0); diff --git a/lib/wallet/client.js b/lib/wallet/client.js index 768f38e50..b39d5bc33 100644 --- a/lib/wallet/client.js +++ b/lib/wallet/client.js @@ -71,6 +71,17 @@ class WalletClient extends NodeClient { return super.setFilter(filter.toRaw()); } + /** + * Check filter against wallet key ring + * @param {WalletKey} ring + * @param {Filter} filter + * @returns {Promise} + */ + + async getBlockFromNode(hash) { + return super.getBlockPeer(hash); + } + async rescan(start) { if (Buffer.isBuffer(start)) start = util.revHex(start); diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index 9f6c43600..afcf0d3c2 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -37,6 +37,13 @@ class NodeClient extends AsyncEmitter { init() { this.node.chain.on('connect', async (entry, block) => { + if (!this.opened || this.node.neutrino) + return; + + await this.emitAsync('block connect', entry, block.txs); + }); + + this.node.chain.on('getblockpeer', async (entry, block) => { if (!this.opened) return; @@ -134,6 +141,10 @@ class NodeClient extends AsyncEmitter { return entry; } + async getBlockFromNode(hash) { + await this.node.chain.getBlockPeer(hash); + } + /** * Send a transaction. Do not wait for promise. * @param {TX} tx diff --git a/lib/wallet/nullclient.js b/lib/wallet/nullclient.js index 744629d4b..f08ca5d6d 100644 --- a/lib/wallet/nullclient.js +++ b/lib/wallet/nullclient.js @@ -130,6 +130,17 @@ class NullClient extends EventEmitter { this.wdb.emit('reset filter'); } + /** + * Check filter against wallet key ring + * @param {WalletKey} ring + * @param {Filter} filter + * @returns {Promise} + */ + + async getBlockFromNode(hash) { + ; + } + /** * Esimate smart fee. * @param {Number?} blocks