diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 233f10e1..86909e31 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -28,6 +28,7 @@ const {OwnershipProof} = require('../covenants/ownership'); const AirdropProof = require('../primitives/airdropproof'); const {CriticalError} = require('../errors'); const thresholdStates = common.thresholdStates; +const scanAction = common.scanAction; const {states} = NameState; const { @@ -2250,7 +2251,7 @@ class Chain extends AsyncEmitter { /** * Scan the blockchain for transactions containing specified address hashes. - * @param {Hash} start - Block hash to start at. + * @param {Hash|Number} start - Block hash or height to start at. * @param {Bloom} filter - Bloom filter containing tx and address hashes. * @param {Function} iter - Iterator. * @returns {Promise} @@ -2265,6 +2266,78 @@ class Chain extends AsyncEmitter { } } + /** + * Interactive scan the blockchain for transactions containing specified + * address hashes. Allows repeat and abort. + * @param {Hash|Number} start - Block hash or height to start at. + * @param {BloomFilter} filter - Starting bloom filter containing tx, + * address and name hashes. + * @param {Function} iter - Iterator. + */ + + async scanInteractive(start, filter, iter) { + if (start == null) + start = this.network.genesis.hash; + + if (typeof start === 'number') + this.logger.info('Scanning(interactive) from height %d.', start); + else + this.logger.info('Scanning(interactive) from block %x.', start); + + let hash = start; + + while (hash != null) { + const unlock = await this.locker.lock(); + + try { + const {entry, txs} = await this.db.scanBlock(hash, filter); + + const action = await iter(entry, txs); + + if (!action || typeof action !== 'object') + throw new Error('Did not get proper action'); + + switch (action.type) { + case scanAction.REPEAT: { + break; + } + case scanAction.REPEAT_SET: { + // try again with updated filter. + filter = action.filter; + break; + } + case scanAction.REPEAT_ADD: { + if (!filter) + throw new Error('No filter set.'); + + for (const chunk of action.chunks) + filter.add(chunk); + break; + } + case scanAction.NEXT: { + const next = await this.getNext(entry); + hash = next && next.hash; + break; + } + case scanAction.ABORT: { + this.logger.info('Scan(interactive) aborted at %x (%d).', + entry.hash, entry.height); + throw new Error('scan request aborted.'); + } + default: + this.logger.debug('Scan(interactive) aborting. Unknown action: %d', + action.type); + throw new Error('Unknown action.'); + } + } catch (e) { + this.logger.debug('Scan(interactive) errored. Error: %s', e.message); + throw e; + } finally { + unlock(); + } + } + } + /** * Add a block to the chain, perform all necessary verification. * @param {Block} block diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index a4fd945a..23976a82 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -1612,6 +1612,51 @@ class ChainDB { this.logger.info('Finished scanning %d blocks.', total); } + /** + * Interactive scans block checks. + * @param {Hash|Number} blockID - Block hash or height to start at. + * @param {BloomFilter} [filter] - Starting bloom filter containing tx, + * address and name hashes. + * @returns {Promise} + */ + + async scanBlock(blockID, filter) { + assert(blockID); + + const entry = await this.getEntry(blockID); + + if (!entry) + throw new Error('Could not find entry.'); + + if (!await this.isMainChain(entry)) + throw new Error('Cannot rescan an alternate chain.'); + + const block = await this.getBlock(entry.hash); + + if (!block) + throw new Error('Block not found.'); + + this.logger.info( + 'Scanning block %x (%d)', + entry.hash, entry.height); + + let txs = []; + + if (!filter) { + txs = block.txs; + } else { + for (const tx of block.txs) { + if (tx.testAndMaybeUpdate(filter)) + txs.push(tx); + } + } + + return { + entry, + txs + }; + } + /** * Save an entry to the database and optionally * connect it as the tip. Note that this method diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index eafc5ac9..06ce9d86 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -69,3 +69,18 @@ exports.flags = { exports.flags.DEFAULT_FLAGS = 0 | exports.flags.VERIFY_POW | exports.flags.VERIFY_BODY; + +/** + * Interactive scan actions. + * @enum {Number} + * @default + */ + +exports.scanAction = { + NONE: 0, + ABORT: 1, + NEXT: 2, + REPEAT_SET: 3, + REPEAT_ADD: 4, + REPEAT: 5 +}; diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index c44ebc8d..1f9dfdfb 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -364,6 +364,18 @@ class FullNode extends Node { return this.chain.scan(start, filter, iter); } + /** + * Interactive rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Bloom} filter + * @param {Function} iter - Iterator. + * @returns {Promise} + */ + + scanInteractive(start, filter, iter) { + return this.chain.scanInteractive(start, filter, iter); + } + /** * Broadcast a transaction. * @param {TX|Block|Claim|AirdropProof} item