diff --git a/CHANGELOG.md b/CHANGELOG.md index 9829e1626..3aca6676d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,16 @@ **When upgrading to this version of hsd, you must pass `--wallet-migrate=3` when you run it for the first time.** +### Primitives +- TX Changes: + - tx.test no longer updates the filter. + - Introduce TX.testAndMaybeUpdate method for potentially updating filter while + testing. (old tx.test) + ### Node Changes + Add support for the interactive rescan, that allows more control over rescan +process and allows parallel rescans. + #### Node HTTP API - `GET /` or `getInfo()` now has more properties: - `treeRootHeight` - height at which the block txns are accumulated @@ -22,6 +31,13 @@ you run it for the first time.** - `compactInterval` - what is the current compaction interval config. - `nextCompaction` - when will the next compaction trigger after restart. - `lastCompaction` - when was the last compaction run. + - Introduce `scan interactive` hook (start, filter) + +### Node HTTP Client: + - Introduce `scanInteractive` method that starts interactive rescan. + - expects ws hook for `block rescan interactive` params `rawEntry, rawTXs` + that returns scanAction object. + - expects ws hook for `block rescan interactive abort` param `message`. ### Wallet Changes #### Configuration diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 233f10e19..aa5bb4c5a 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 scanActions = common.scanActions; 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 scanActions.REPEAT: { + break; + } + case scanActions.REPEAT_SET: { + // try again with updated filter. + filter = action.filter; + break; + } + case scanActions.REPEAT_ADD: { + if (!filter) + throw new Error('No filter set.'); + + for (const chunk of action.chunks) + filter.add(chunk); + break; + } + case scanActions.NEXT: { + const next = await this.getNext(entry); + hash = next && next.hash; + break; + } + case scanActions.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 63ab65ae8..7f075349b 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -1600,7 +1600,7 @@ class ChainDB { entry.hash, entry.height); for (const tx of block.txs) { - if (tx.test(filter)) + if (tx.testAndMaybeUpdate(filter)) txs.push(tx); } @@ -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 != null); + + 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 eafc5ac97..2a45e2d45 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.scanActions = { + NONE: 0, + ABORT: 1, + NEXT: 2, + REPEAT_SET: 3, + REPEAT_ADD: 4, + REPEAT: 5 +}; diff --git a/lib/client/node.js b/lib/client/node.js index 3d0bca6c4..4bf1ae02d 100644 --- a/lib/client/node.js +++ b/lib/client/node.js @@ -334,6 +334,22 @@ class NodeClient extends Client { return this.call('rescan', start); } + + /** + * Rescan for any missed transactions. (Interactive) + * @param {Number|Hash} start - Start block. + * @param {BloomFilter} [filter] + * @returns {Promise} + */ + + rescanInteractive(start, filter = null) { + if (start == null) + start = 0; + + assert(typeof start === 'number' || Buffer.isBuffer(start)); + + return this.call('rescan interactive', start, filter); + } } /* diff --git a/lib/net/peer.js b/lib/net/peer.js index 65ffcafd7..96b3bdcca 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -620,7 +620,7 @@ class Peer extends EventEmitter { // Check the peer's bloom // filter if they're using spv. if (this.spvFilter) { - if (!tx.test(this.spvFilter)) + if (!tx.testAndMaybeUpdate(this.spvFilter)) continue; } diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index c44ebc8d8..de7fcc2b0 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -355,7 +355,7 @@ class FullNode extends Node { /** * Rescan for any missed transactions. * @param {Number|Hash} start - Start block. - * @param {Bloom} filter + * @param {BloomFilter} filter * @param {Function} iter - Iterator. * @returns {Promise} */ @@ -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 {BloomFilter} 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 diff --git a/lib/node/http.js b/lib/node/http.js index 23b6621cb..e82b482bd 100644 --- a/lib/node/http.js +++ b/lib/node/http.js @@ -20,6 +20,7 @@ const TX = require('../primitives/tx'); const Claim = require('../primitives/claim'); const Address = require('../primitives/address'); const Network = require('../protocol/network'); +const scanActions = require('../blockchain/common').scanActions; const pkg = require('../pkg'); /** @@ -706,6 +707,21 @@ class HTTP extends Server { return this.scan(socket, start); }); + + socket.hook('rescan interactive', (...args) => { + const valid = new Validator(args); + const start = valid.uintbhash(0); + const rawFilter = valid.buf(1); + let filter = socket.filter; + + if (start == null) + throw new Error('Invalid parameter.'); + + if (rawFilter) + filter = BloomFilter.fromRaw(rawFilter); + + return this.scanInteractive(socket, start, filter); + }); } /** @@ -813,7 +829,7 @@ class HTTP extends Server { if (!socket.filter) return false; - return tx.test(socket.filter); + return tx.testAndMaybeUpdate(socket.filter); } /** @@ -834,6 +850,82 @@ class HTTP extends Server { return socket.call('block rescan', block, raw); }); + + return null; + } + + /** + * Scan using a socket's filter (interactive). + * @param {WebSocket} socket + * @param {Hash} start + * @param {BloomFilter} filter + * @returns {Promise} + */ + + async scanInteractive(socket, start, filter) { + const iter = async (entry, txs) => { + const block = entry.encode(); + const raw = []; + + for (const tx of txs) + raw.push(tx.encode()); + + const action = await socket.call('block rescan interactive', block, raw); + const valid = new Validator(action); + const actionType = valid.i32('type'); + + switch (actionType) { + case scanActions.NEXT: + case scanActions.ABORT: + case scanActions.REPEAT: { + return { + type: actionType + }; + } + case scanActions.REPEAT_SET: { + // NOTE: This is operation is on the heavier side, + // because it sends the whole Filter that can be quite + // big depending on the situation. + // NOTE: In HTTP Context REPEAT_SET wont modify socket.filter + // but instead setup new one for the rescan. Further REPEAT_ADDs will + // modify this filter instead of the socket.filter. + const rawFilter = valid.buf('filter'); + let filter = null; + + if (rawFilter != null) + filter = BloomFilter.fromRaw(rawFilter); + + return { + type: scanActions.REPEAT_SET, + filter: filter + }; + } + case scanActions.REPEAT_ADD: { + // NOTE: This operation depending on the filter + // that was provided can be either modifying the + // socket.filter or the filter provided by REPEAT_SET. + const chunks = valid.array('chunks'); + + if (!chunks) + throw new Error('Invalid parameter.'); + + return { + type: scanActions.REPEAT_ADD, + chunks: chunks + }; + } + + default: + throw new Error('Unknown action.'); + } + }; + + try { + await this.node.scanInteractive(start, filter, iter); + } catch (err) { + return socket.call('block rescan interactive abort', err.message); + } + return null; } } diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 7b81fb55b..5145325d8 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -226,10 +226,24 @@ class SPVNode extends Node { * Scan for any missed transactions. * Note that this will replay the blockchain sync. * @param {Number|Hash} start - Start block. + * @param {BloomFilter} filter + * @param {Function} iter * @returns {Promise} */ - async scan(start) { + async scan(start, filter, iter) { + throw new Error('Not implemented.'); + } + + /** + * Interactive scan for any missed transactions. + * @param {Number|Hash} start + * @param {BloomFilter} filter + * @param {Function} iter + * @returns {Promise} + */ + + scanInteractive(start, filter, iter) { throw new Error('Not implemented.'); } diff --git a/lib/primitives/merkleblock.js b/lib/primitives/merkleblock.js index fbf004e19..636469b1d 100644 --- a/lib/primitives/merkleblock.js +++ b/lib/primitives/merkleblock.js @@ -432,7 +432,7 @@ class MerkleBlock extends AbstractBlock { const matches = []; for (const tx of block.txs) - matches.push(tx.test(filter) ? 1 : 0); + matches.push(tx.testAndMaybeUpdate(filter) ? 1 : 0); return this.fromMatches(block, matches); } diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index 0f016c093..c1bc6b6d8 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -1574,6 +1574,31 @@ class TX extends bio.Struct { */ test(filter) { + if (filter.test(this.hash())) + return true; + + for (let i = 0; i < this.outputs.length; i++) { + const {address, covenant} = this.outputs[i]; + + if (filter.test(address.hash) || covenant.test(filter)) + return true; + } + + for (const {prevout} of this.inputs) { + if (filter.test(prevout.encode())) + return true; + } + + return false; + } + + /** + * Test a transaction against a bloom filter. + * @param {BloomFilter} filter + * @returns {Boolean} + */ + + testAndMaybeUpdate(filter) { let found = false; if (filter.test(this.hash())) diff --git a/package-lock.json b/package-lock.json index ac938796b..a592c1191 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "bmutex": "~0.1.6", "bns": "~0.15.0", "bsert": "~0.0.12", - "bsock": "~0.1.9", + "bsock": "~0.1.11", "bsocks": "~0.2.6", "btcp": "~0.1.5", "buffer-map": "~0.0.7", @@ -310,11 +310,11 @@ } }, "node_modules/bsock": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/bsock/-/bsock-0.1.9.tgz", - "integrity": "sha512-/l9Kg/c5o+n/0AqreMxh2jpzDMl1ikl4gUxT7RFNe3A3YRIyZkiREhwcjmqxiymJSRI/Qhew357xGn1SLw/xEw==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/bsock/-/bsock-0.1.11.tgz", + "integrity": "sha512-4COhlKKBfOQOomNvz1hjoPtN5ytpqbxkuCPvIPbYMvaZwNBKpo8dZa1LJjcN3wuAkjIIJLF/fc4o3e1DYiceQw==", "dependencies": { - "bsert": "~0.0.10" + "bsert": "~0.0.12" }, "engines": { "node": ">=8.0.0" diff --git a/package.json b/package.json index 0d4e0de30..aae0c4761 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "bmutex": "~0.1.6", "bns": "~0.15.0", "bsert": "~0.0.12", - "bsock": "~0.1.9", + "bsock": "~0.1.11", "bsocks": "~0.2.6", "btcp": "~0.1.5", "buffer-map": "~0.0.7", diff --git a/test/auction-rpc-test.js b/test/auction-rpc-test.js index 6e27efdae..e479e5e31 100644 --- a/test/auction-rpc-test.js +++ b/test/auction-rpc-test.js @@ -2,111 +2,55 @@ const assert = require('bsert'); const bio = require('bufio'); -const plugin = require('../lib/wallet/plugin'); const rules = require('../lib/covenants/rules'); const common = require('./util/common'); const { ChainEntry, - FullNode, - SPVNode, KeyRing, MTX, Network, Path } = require('..'); -const {NodeClient, WalletClient} = require('../lib/client'); const {forValue} = require('./util/common'); +const NodeContext = require('./util/node'); class TestUtil { - constructor(options) { - if (!options) - options = Object.create(null); - - if (!options.host) - options.host = '127.0.0.1'; - - if (!options.nport) - options.nport = 14037; - - if (!options.wport) - options.wport = 14039; - - this.network = Network.get('regtest'); - - this.txs = {}; - - this.blocks = {}; - - this.node = new FullNode({ + constructor() { + this.nodeCtx = new NodeContext({ memory: true, workers: true, - network: this.network.type, listen: true, - bip37: true + bip37: true, + wallet: true }); + this.network = this.nodeCtx.network; + this.txs = {}; + this.blocks = {}; - this.node.use(plugin); - - this.nclient = new NodeClient({ - timeout: 15000, - host: options.host, - port: options.nport - }); + this.node = this.nodeCtx.node; + } - this.wclient = new WalletClient({ - host: options.host, - port: options.wport - }); + get nclient() { + return this.nodeCtx.nclient; } - /** - * Execute an RPC using the wallet client. - * @param {String} method - RPC method - * @param {Array} params - method parameters - * @returns {Promise} - Returns a two item array with the RPC's return value - * or null as the first item and an error or null as the second item. - */ - - async wrpc(method, params = []) { - return this.wclient.execute(method, params) - .then(data => data) - .catch((err) => { - throw new Error(err); - }); + get wclient() { + return this.nodeCtx.wclient; } - /** - * Execute an RPC using the node client. - * @param {String} method - RPC method - * @param {Array} params - method parameters - * @returns {Promise} - Returns a two item array with the - * RPC's return value or null as the first item and an error or - * null as the second item. - */ - - async nrpc(method, params = []) { - return this.nclient.execute(method, params) - .then(data => data) - .catch((err) => { - throw new Error(err); - }); + wrpc(method, params = []) { + return this.nodeCtx.wrpc(method, params); } - /** - * Open the util and all its child objects. - */ + nrpc(method, params = []) { + return this.nodeCtx.nrpc(method, params); + } async open() { assert(!this.opened, 'TestUtil is already open.'); this.opened = true; - await this.node.ensure(); - await this.node.open(); - await this.node.connect(); - this.node.startSync(); - - await this.nclient.open(); - await this.wclient.open(); + await this.nodeCtx.open(); this.node.plugins.walletdb.wdb.on('confirmed', ((details, tx) => { const txid = tx.txid(); @@ -125,17 +69,8 @@ class TestUtil { }); } - /** - * Close util and all its child objects. - */ - async close() { - assert(this.opened, 'TestUtil is not open.'); - this.opened = false; - - await this.nclient.close(); - await this.wclient.close(); - await this.node.close(); + await this.nodeCtx.close(); } async confirmTX(txid, timeout = 5000) { @@ -480,43 +415,34 @@ describe('Auction RPCs', function() { }); describe('SPV', function () { - const spvNode = new SPVNode({ - memory: true, - network: 'regtest', + const spvCtx = new NodeContext({ port: 10000, brontidePort: 20000, httpPort: 30000, only: '127.0.0.1', - noDns: true - }); + noDns: true, - const spvClient = new NodeClient({ - port: 30000 + spv: true }); before(async () => { await util.node.connect(); - await spvNode.open(); - await spvNode.connect(); - await spvNode.startSync(); - - await forValue(spvNode.chain, 'height', util.node.chain.height); + await spvCtx.open(); - await spvClient.open(); + await forValue(spvCtx.chain, 'height', util.node.chain.height); }); after(async () => { - await spvClient.close(); - await spvNode.close(); + await spvCtx.close(); }); it('should not get current namestate', async () => { - const {info} = await spvClient.execute('getnameinfo', [name]); + const {info} = await spvCtx.nrpc('getnameinfo', [name]); assert.strictEqual(info, null); }); it('should get historcial namestate at safe height', async () => { - const {info} = await spvClient.execute('getnameinfo', [name, true]); + const {info} = await spvCtx.nrpc('getnameinfo', [name, true]); assert.strictEqual(info.name, name); assert.strictEqual(info.state, 'CLOSED'); assert.strictEqual(info.value, loserBid.bid * COIN); @@ -524,12 +450,12 @@ describe('Auction RPCs', function() { }); it('should not get current resource', async () => { - const json = await spvClient.execute('getnameresource', [name]); + const json = await spvCtx.nrpc('getnameresource', [name]); assert.strictEqual(json, null); }); it('should get historcial resource at safe height', async () => { - const json = await spvClient.execute('getnameresource', [name, true]); + const json = await spvCtx.nrpc('getnameresource', [name, true]); assert.deepStrictEqual( json, { @@ -546,7 +472,7 @@ describe('Auction RPCs', function() { it('should not verifymessagewithname', async () => { // No local Urkel tree, namestate is always null await assert.rejects( - spvClient.execute('verifymessagewithname', [name, signSig, signMsg]), + spvCtx.nrpc('verifymessagewithname', [name, signSig, signMsg]), {message: /Cannot find the name owner/} ); }); @@ -555,7 +481,7 @@ describe('Auction RPCs', function() { // This time we do have a valid namestate to work with, but // SPV nodes still don't have a UTXO set to get addresses from await assert.rejects( - spvClient.execute('verifymessagewithname', [name, signSig, signMsg, true]), + spvCtx.nrpc('verifymessagewithname', [name, signSig, signMsg, true]), {message: /Cannot find the owner's address/} ); }); diff --git a/test/chain-reset-reorg-test.js b/test/chain-reset-reorg-test.js index 0dfb93e06..132c9fbfa 100644 --- a/test/chain-reset-reorg-test.js +++ b/test/chain-reset-reorg-test.js @@ -9,16 +9,16 @@ const { openChainBundle, closeChainBundle, syncChain, - chainTreeHas, - chainTxnHas + chainTreeHasName, + chainTxnHasName } = require('./util/chain'); const network = Network.get('regtest'); describe('Chain reorg/reset test', function() { let wallet; - let chainb1, chain, mainMiner; - let chainb2, altChain, altMiner; + let chainBundle1, chain, mainMiner; + let chainBundle2, altChain, altMiner; let tipHeight = 0; const mineBlocksOpens = async (miner, n) => { @@ -58,38 +58,38 @@ describe('Chain reorg/reset test', function() { wallet = new MemWallet({ network }); - chainb1 = getChainBundle({ + chainBundle1 = getChainBundle({ memory: true, workers: true, address: wallet.getReceive() }); - chainb2 = getChainBundle({ + chainBundle2 = getChainBundle({ memory: true, workers: true, address: wallet.getReceive() }); - chainb1.chain.on('connect', (entry, block) => { + chainBundle1.chain.on('connect', (entry, block) => { wallet.addBlock(entry, block.txs); }); - chainb1.chain.on('disconnect', (entry, block) => { + chainBundle1.chain.on('disconnect', (entry, block) => { wallet.removeBlock(entry, block.txs); }); - await openChainBundle(chainb1); - await openChainBundle(chainb2); + await openChainBundle(chainBundle1); + await openChainBundle(chainBundle2); - chain = chainb1.chain; - mainMiner = chainb1.miner; - altChain = chainb2.chain; - altMiner = chainb2.miner; + chain = chainBundle1.chain; + mainMiner = chainBundle1.miner; + altChain = chainBundle2.chain; + altMiner = chainBundle2.miner; }; const afterHook = async () => { - await closeChainBundle(chainb1); - await closeChainBundle(chainb2); + await closeChainBundle(chainBundle1); + await closeChainBundle(chainBundle2); }; describe('Chain reorg', function() { @@ -124,8 +124,8 @@ describe('Chain reorg/reset test', function() { tipHeight++; for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } const root = await chain.db.treeRoot(); @@ -136,8 +136,8 @@ describe('Chain reorg/reset test', function() { assert.bufferEqual(chain.db.treeRoot(), root); for (const name of [...names0, ...names1]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } // mine 3 blocks on alt chain @@ -148,14 +148,14 @@ describe('Chain reorg/reset test', function() { assert.bufferEqual(chain.db.treeRoot(), root); for (const name of [...names0, ...names2]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } // these got reorged. for (const name of names1) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } assert.strictEqual(chain.tip.height, tipHeight); @@ -167,13 +167,13 @@ describe('Chain reorg/reset test', function() { tipHeight++; for (const name of [...names0, ...names2, ...names3]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of names1) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } assert.notBufferEqual(chain.db.treeRoot(), root); @@ -195,8 +195,8 @@ describe('Chain reorg/reset test', function() { tipHeight += 3; for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } assert.notBufferEqual(chain.db.txn.rootHash(), root); @@ -208,30 +208,30 @@ describe('Chain reorg/reset test', function() { assert.strictEqual(chain.tip.height, tipHeight + 3); for (const name of [...names0, ...names1.slice(0, -1)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } - assert.strictEqual(await chainTreeHas(chain, names1[names1.length - 1]), false); - assert.strictEqual(await chainTxnHas(chain, names1[names1.length - 1]), true); + assert.strictEqual(await chainTreeHasName(chain, names1[names1.length - 1]), false); + assert.strictEqual(await chainTxnHasName(chain, names1[names1.length - 1]), true); const names2 = await mineBlocksOpens(altMiner, 4); await syncChain(altChain, chain, tipHeight); tipHeight += 4; for (const name of [...names0, ...names2.slice(0, -2)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of [...names2.slice(-2)]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of names1) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } }); @@ -244,13 +244,13 @@ describe('Chain reorg/reset test', function() { assert.strictEqual(chain.tip.height, tipHeight + 15); for (const name of [...names1.slice(0, -2)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of [...names1.slice(-2)]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } // mine 16 on alt chain. @@ -262,18 +262,18 @@ describe('Chain reorg/reset test', function() { assert.strictEqual(chain.tip.height, tipHeight); for (const name of [...names2.slice(0, -3)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of [...names2.slice(-3)]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of names1) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } }); }); @@ -313,8 +313,8 @@ describe('Chain reorg/reset test', function() { tipHeight += 2; for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } const root = await chain.db.treeRoot(); @@ -327,26 +327,26 @@ describe('Chain reorg/reset test', function() { assert.bufferEqual(chain.db.treeRoot(), root); for (const name of [...names0, ...resetNames]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } await chain.reset(tipHeight - 2); for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of resetNames) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } await syncChain(altChain, chain, tipHeight - 2); for (const name of [...names0, ...resetNames]) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } }); @@ -358,8 +358,8 @@ describe('Chain reorg/reset test', function() { tipHeight += 3; for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } const resetNames = await mineBlocksOpens(mainMiner, 3); @@ -367,34 +367,34 @@ describe('Chain reorg/reset test', function() { tipHeight += 3; for (const name of [...names0, ...resetNames.slice(0, -1)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } const txnName = resetNames[resetNames.length - 1]; - assert.strictEqual(await chainTreeHas(chain, txnName), false); - assert.strictEqual(await chainTxnHas(chain, txnName), true); + assert.strictEqual(await chainTreeHasName(chain, txnName), false); + assert.strictEqual(await chainTxnHasName(chain, txnName), true); await chain.reset(tipHeight - 3); for (const name of names0) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of resetNames) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), false); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), false); } await syncChain(altChain, chain, tipHeight - 3); for (const name of [...names0, ...resetNames.slice(0, -1)]) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } - assert.strictEqual(await chainTreeHas(chain, txnName), false); - assert.strictEqual(await chainTxnHas(chain, txnName), true); + assert.strictEqual(await chainTreeHasName(chain, txnName), false); + assert.strictEqual(await chainTxnHasName(chain, txnName), true); }); it('should mine 18 blocks, reset and resync', async () => { @@ -411,13 +411,13 @@ describe('Chain reorg/reset test', function() { const txnNames = names.slice(-3); for (const name of treeNames) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of txnNames) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } await chain.reset(tipHeight - 18); @@ -427,13 +427,13 @@ describe('Chain reorg/reset test', function() { assert.strictEqual(altChain.tip.height, tipHeight); for (const name of treeNames) { - assert.strictEqual(await chainTreeHas(chain, name), true); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), true); + assert.strictEqual(await chainTxnHasName(chain, name), true); } for (const name of txnNames) { - assert.strictEqual(await chainTreeHas(chain, name), false); - assert.strictEqual(await chainTxnHas(chain, name), true); + assert.strictEqual(await chainTreeHasName(chain, name), false); + assert.strictEqual(await chainTxnHasName(chain, name), true); } }); }); diff --git a/test/mempool-invalidation-test.js b/test/mempool-invalidation-test.js index e146af70d..fa4e302bd 100644 --- a/test/mempool-invalidation-test.js +++ b/test/mempool-invalidation-test.js @@ -3,13 +3,13 @@ const assert = require('bsert'); const {BufferMap} = require('buffer-map'); const Network = require('../lib/protocol/network'); -const FullNode = require('../lib/node/fullnode'); const {ownership} = require('../lib/covenants/ownership'); const rules = require('../lib/covenants/rules'); const {states} = require('../lib/covenants/namestate'); const {Resource} = require('../lib/dns/resource'); const {forEvent} = require('./util/common'); const {CachedStubResolver} = require('./util/stub'); +const NodeContext = require('./util/node'); const network = Network.get('regtest'); const { @@ -48,40 +48,41 @@ describe('Mempool Invalidation', function() { describe('Covenant invalidation (Integration)', function() { this.timeout(3000); + let nodeCtx; let node, wallet, wallet2; const getNameState = async (name) => { - const ns = await node.chain.db.getNameStateByName(name); + const ns = await nodeCtx.chain.db.getNameStateByName(name); if (!ns) return null; - return ns.state(node.chain.tip.height + 1, network); + return ns.state(nodeCtx.chain.tip.height + 1, network); }; const isExpired = async (name) => { - const ns = await node.chain.db.getNameStateByName(name); + const ns = await nodeCtx.chain.db.getNameStateByName(name); if (!ns) return true; - return ns.isExpired(node.chain.tip.height + 1, network); + return ns.isExpired(nodeCtx.chain.tip.height + 1, network); }; before(async () => { network.names.renewalWindow = 200; - node = new FullNode({ + nodeCtx = new NodeContext({ + network: 'regtest', memory: true, - network: network.type, - plugins: [require('../lib/wallet/plugin')] + wallet: true }); - await node.ensure(); - await node.open(); + await nodeCtx.open(); + + node = nodeCtx.node; - const walletPlugin = node.require('walletdb'); - const wdb = walletPlugin.wdb; + const wdb = nodeCtx.wdb; wallet = await wdb.get('primary'); wallet2 = await wdb.create({ id: 'secondary' @@ -93,7 +94,7 @@ describe('Mempool Invalidation', function() { for (let i = 0; i < treeInterval; i++) await mineBlock(node); - const fundTX = forEvent(node.mempool, 'tx', 1, 2000); + const fundTX = forEvent(nodeCtx.mempool, 'tx', 1, 2000); const w2addr = (await wallet2.receiveAddress('default')).toString(); await wallet.send({ @@ -115,7 +116,8 @@ describe('Mempool Invalidation', function() { after(async () => { network.names.renewalWindow = ACTUAL_RENEWAL_WINDOW; - await node.close(); + await nodeCtx.close(); + await nodeCtx.destroy(); }); it('should invalidate opens', async () => { @@ -341,26 +343,26 @@ describe('Mempool Invalidation', function() { describe('Claim Invalidation (Integration)', function() { this.timeout(50000); + let nodeCtx; let node, wallet; // copy names const TEST_CLAIMS = NAMES.slice(); before(async () => { - node = new FullNode({ - memory: true, + nodeCtx = new NodeContext({ network: network.type, - plugins: [require('../lib/wallet/plugin')] + memory: true, + wallet: true }); - await node.ensure(); - await node.open(); + await nodeCtx.open(); + node = nodeCtx.node; // Ignore claim validation ownership.ignore = true; - const walletPlugin = node.require('walletdb'); - const wdb = walletPlugin.wdb; + const wdb = nodeCtx.wdb; wallet = await wdb.get('primary'); const addr = await wallet.receiveAddress('default'); @@ -376,7 +378,8 @@ describe('Mempool Invalidation', function() { after(async () => { network.names.claimPeriod = ACTUAL_CLAIM_PERIOD; - await node.close(); + await nodeCtx.close(); + await nodeCtx.destroy(); }); it('should mine an interval', async () => { @@ -444,26 +447,26 @@ describe('Mempool Invalidation', function() { describe('Claim Invalidation on reorg (Integration)', function() { this.timeout(50000); + let nodeCtx; let node, wallet; // copy names const TEST_CLAIMS = NAMES.slice(); before(async () => { - node = new FullNode({ - memory: true, + nodeCtx = new NodeContext({ network: network.type, - plugins: [require('../lib/wallet/plugin')] + memory: true, + wallet: true }); - await node.ensure(); - await node.open(); + await nodeCtx.open(); // Ignore claim validation ownership.ignore = true; - const walletPlugin = node.require('walletdb'); - const wdb = walletPlugin.wdb; + node = nodeCtx.node; + const wdb = nodeCtx.wdb; wallet = await wdb.get('primary'); const addr = await wallet.receiveAddress('default'); @@ -479,7 +482,8 @@ describe('Mempool Invalidation', function() { after(async () => { network.names.claimPeriod = ACTUAL_CLAIM_PERIOD; - await node.close(); + await nodeCtx.close(); + await nodeCtx.destroy(); }); it('should mine an interval', async () => { diff --git a/test/node-http-test.js b/test/node-http-test.js index 148ecc2f3..eedb06c13 100644 --- a/test/node-http-test.js +++ b/test/node-http-test.js @@ -2,10 +2,7 @@ const assert = require('bsert'); const bio = require('bufio'); -const NodeClient = require('../lib/client/node'); const Network = require('../lib/protocol/network'); -const FullNode = require('../lib/node/fullnode'); -const SPVNode = require('../lib/node/spvnode'); const Address = require('../lib/primitives/address'); const Mnemonic = require('../lib/hd/mnemonic'); const Witness = require('../lib/script/witness'); @@ -16,40 +13,32 @@ const Coin = require('../lib/primitives/coin'); const MTX = require('../lib/primitives/mtx'); const rules = require('../lib/covenants/rules'); const common = require('./util/common'); +const NodeContext = require('./util/node'); const mnemonics = require('./data/mnemonic-english.json'); -const {forEvent} = common; + // Commonly used test mnemonic const phrase = mnemonics[0][1]; describe('Node HTTP', function() { describe('Chain info', function() { - const network = Network.get('regtest'); - const nclient = new NodeClient({ - port: network.rpcPort - }); - - let node; + let nodeCtx; afterEach(async () => { - if (node && node.opened) { - const close = forEvent(node, 'close'); - await node.close(); - await close; - } + if (nodeCtx && nodeCtx.opened) + await nodeCtx.close(); - node = null; + nodeCtx = null; }); it('should get full node chain info', async () => { - node = new FullNode({ - network: network.type + nodeCtx = new NodeContext({ + network: 'regtest' }); - await node.open(); - - const {chain} = await nclient.getInfo(); + await nodeCtx.open(); + const {chain} = await nodeCtx.nclient.getInfo(); assert.strictEqual(chain.height, 0); - assert.strictEqual(chain.tip, network.genesis.hash.toString('hex')); + assert.strictEqual(chain.tip, nodeCtx.network.genesis.hash.toString('hex')); assert.strictEqual(chain.treeRoot, Buffer.alloc(32, 0).toString('hex')); assert.strictEqual(chain.progress, 0); assert.strictEqual(chain.indexers.indexTX, false); @@ -64,41 +53,40 @@ describe('Node HTTP', function() { }); it('should get fullnode chain info with indexers', async () => { - node = new FullNode({ - network: network.type, + nodeCtx = new NodeContext({ + network: 'regtest', indexAddress: true, indexTX: true }); - await node.open(); - - const {chain} = await nclient.getInfo(); + await nodeCtx.open(); + const {chain} = await nodeCtx.nclient.getInfo(); assert.strictEqual(chain.indexers.indexTX, true); assert.strictEqual(chain.indexers.indexAddress, true); }); it('should get fullnode chain info with pruning', async () => { - node = new FullNode({ - network: network.type, + nodeCtx = new NodeContext({ + network: 'regtest', prune: true }); - await node.open(); + await nodeCtx.open(); - const {chain} = await nclient.getInfo(); + const {chain} = await nodeCtx.nclient.getInfo(); assert.strictEqual(chain.options.prune, true); }); it('should get fullnode chain info with compact', async () => { - node = new FullNode({ - network: network.type, + nodeCtx = new NodeContext({ + network: 'regtest', compactTreeOnInit: true, compactTreeInitInterval: 20000 }); - await node.open(); + await nodeCtx.open(); - const {chain} = await nclient.getInfo(); + const {chain} = await nodeCtx.nclient.getInfo(); assert.strictEqual(chain.treeCompaction.compacted, false); assert.strictEqual(chain.treeCompaction.compactOnInit, true); assert.strictEqual(chain.treeCompaction.compactInterval, 20000); @@ -109,24 +97,29 @@ describe('Node HTTP', function() { }); it('should get spv node chain info', async () => { - node = new SPVNode({ - network: network.type + nodeCtx = new NodeContext({ + network: 'regtest', + spv: true }); - await node.open(); + await nodeCtx.open(); - const {chain} = await nclient.getInfo(); + const {chain} = await nodeCtx.nclient.getInfo(); assert.strictEqual(chain.options.spv, true); }); it('should get next tree update height', async () => { const someAddr = 'rs1q7q3h4chglps004u3yn79z0cp9ed24rfrhvrxnx'; - node = new FullNode({ - network: network.type + nodeCtx = new NodeContext({ + network: 'regtest' }); - const interval = network.names.treeInterval; - await node.open(); + const interval = nodeCtx.network.names.treeInterval; + + await nodeCtx.open(); + + const nclient = nodeCtx.nclient; + const node = nodeCtx.node; { // 0th block will be 0. @@ -164,24 +157,15 @@ describe('Node HTTP', function() { describe('Networking info', function() { it('should not have public address: regtest', async () => { - const network = Network.get('regtest'); + const nodeCtx = new NodeContext(); - const node = new FullNode({ - network: network.type - }); - - const nclient = new NodeClient({ - port: network.rpcPort - }); - - await node.open(); - await node.connect(); - const {pool} = await nclient.getInfo(); - await node.close(); + await nodeCtx.open(); + const {pool} = await nodeCtx.nclient.getInfo(); + await nodeCtx.close(); assert.strictEqual(pool.host, '0.0.0.0'); - assert.strictEqual(pool.port, network.port); - assert.strictEqual(pool.brontidePort, network.brontidePort); + assert.strictEqual(pool.port, nodeCtx.network.port); + assert.strictEqual(pool.brontidePort, nodeCtx.network.brontidePort); const {public: pub} = pool; @@ -194,19 +178,13 @@ describe('Node HTTP', function() { it('should not have public address: regtest, listen', async () => { const network = Network.get('regtest'); - const node = new FullNode({ - network: network.type, + const nodeCtx = new NodeContext({ listen: true }); - const nclient = new NodeClient({ - port: network.rpcPort - }); - - await node.open(); - await node.connect(); - const {pool} = await nclient.getInfo(); - await node.close(); + await nodeCtx.open(); + const {pool} = await nodeCtx.nclient.getInfo(); + await nodeCtx.close(); assert.strictEqual(pool.host, '0.0.0.0'); assert.strictEqual(pool.port, network.port); @@ -221,20 +199,15 @@ describe('Node HTTP', function() { }); it('should not have public address: main', async () => { - const network = Network.get('main'); - - const node = new FullNode({ - network: network.type + const nodeCtx = new NodeContext({ + network: 'main' }); - const nclient = new NodeClient({ - port: network.rpcPort - }); + const network = nodeCtx.network; - await node.open(); - await node.connect(); - const {pool} = await nclient.getInfo(); - await node.close(); + await nodeCtx.open(); + const {pool} = await nodeCtx.nclient.getInfo(); + await nodeCtx.close(); assert.strictEqual(pool.host, '0.0.0.0'); assert.strictEqual(pool.port, network.port); @@ -249,21 +222,16 @@ describe('Node HTTP', function() { }); it('should not have public address: main, listen', async () => { - const network = Network.get('main'); - - const node = new FullNode({ - network: network.type, + const nodeCtx = new NodeContext({ + network: 'main', listen: true }); - const nclient = new NodeClient({ - port: network.rpcPort - }); + const network = nodeCtx.network; - await node.open(); - await node.connect(); - const {pool} = await nclient.getInfo(); - await node.close(); + await nodeCtx.open(); + const {pool} = await nodeCtx.nclient.getInfo(); + await nodeCtx.close(); assert.strictEqual(pool.host, '0.0.0.0'); assert.strictEqual(pool.port, network.port); @@ -278,27 +246,23 @@ describe('Node HTTP', function() { }); it('should have public address: main, listen, publicHost', async () => { - const network = Network.get('main'); const publicHost = '100.200.11.22'; const publicPort = 11111; const publicBrontidePort = 22222; - const node = new FullNode({ - network: network.type, + const nodeCtx = new NodeContext({ + network: 'main', listen: true, publicHost, publicPort, publicBrontidePort }); - const nclient = new NodeClient({ - port: network.rpcPort - }); + const network = nodeCtx.network; - await node.open(); - await node.connect(); - const {pool} = await nclient.getInfo(); - await node.close(); + await nodeCtx.open(); + const {pool} = await nodeCtx.nclient.getInfo(); + await nodeCtx.close(); assert.strictEqual(pool.host, '0.0.0.0'); assert.strictEqual(pool.port, network.port); @@ -317,25 +281,18 @@ describe('Node HTTP', function() { this.timeout(15000); describe('tree commit', () => { - const network = Network.get('regtest'); const {types} = rules; - const node = new FullNode({ - network: 'regtest', + const nodeCtx = new NodeContext({ apiKey: 'foo', - walletAuth: true, - memory: true, indexTx: true, indexAddress: true, rejectAbsurdFees: false }); - - const nclient = new NodeClient({ - port: network.rpcPort, - apiKey: 'foo' - }); + const network = nodeCtx.network; const {treeInterval} = network.names; + const nclient = nodeCtx.nclient; let privkey, pubkey; let socketData, mempoolData; @@ -345,7 +302,7 @@ describe('Node HTTP', function() { async function mineBlocks(count, address) { for (let i = 0; i < count; i++) { const obj = { complete: false }; - node.once('block', () => { + nodeCtx.node.once('block', () => { obj.complete = true; }); await nclient.execute('generatetoaddress', [1, address]); @@ -354,8 +311,7 @@ describe('Node HTTP', function() { } before(async () => { - await node.open(); - await nclient.open(); + await nodeCtx.open(); await nclient.call('watch chain'); const mnemonic = Mnemonic.fromPhrase(phrase); @@ -379,14 +335,13 @@ describe('Node HTTP', function() { socketData.push({root, entry, block}); }); - node.mempool.on('tx', (tx) => { + nodeCtx.mempool.on('tx', (tx) => { mempoolData[tx.txid()] = true; }); }); after(async () => { - await nclient.close(); - await node.close(); + await nodeCtx.close(); }); beforeEach(() => { @@ -423,7 +378,7 @@ describe('Node HTTP', function() { coins.sort((a, b) => a.height - b.height); const coin = Coin.fromJSON(coins[0]); - assert.ok(node.chain.height > coin.height + network.coinbaseMaturity); + assert.ok(nodeCtx.chain.height > coin.height + network.coinbaseMaturity); mtx.addCoin(coin); const addr = Address.fromPubkey(pubkey); @@ -436,7 +391,7 @@ describe('Node HTTP', function() { assert.ok(valid); const tx = mtx.toTX(); - await node.sendTX(tx); + await nodeCtx.node.sendTX(tx); await common.forValue(mempoolData, tx.txid(), true); @@ -449,7 +404,7 @@ describe('Node HTTP', function() { assert.equal(socketData.length, 1); const {root, block, entry} = socketData[0]; - assert.bufferEqual(node.chain.db.treeRoot(), root); + assert.bufferEqual(nodeCtx.chain.db.treeRoot(), root); const info = await nclient.getInfo(); assert.notEqual(pre.chain.tip, info.chain.tip); diff --git a/test/node-rescan-test.js b/test/node-rescan-test.js new file mode 100644 index 000000000..75783b523 --- /dev/null +++ b/test/node-rescan-test.js @@ -0,0 +1,852 @@ +'use strict'; + +const assert = require('bsert'); +const {BufferSet} = require('buffer-map'); +const {BloomFilter} = require('@handshake-org/bfilter'); +const TX = require('../lib/primitives/tx'); +const nodeCommon = require('../lib/blockchain/common'); +const {scanActions} = nodeCommon; +const NodeContext = require('./util/node'); +const {forEvent, sleep} = require('./util/common'); +const MemWallet = require('./util/memwallet'); +const rules = require('../lib/covenants/rules'); + +describe('Node Rescan Interactive API', function() { + const TIMEOUT = 10000; + + this.timeout(TIMEOUT); + + /** @type {NodeContext} */ + let nodeCtx; + let funderWallet; + + const RESCAN_DEPTH = 10; + const names = []; + const addresses = []; + // store txs by height. + const allTXs = {}; + const addressTXs = {}; + const txHashTXs = {}; + const nameHashTXs = {}; + // use smaller filters than the default + const addressFilter = BloomFilter.fromRate(10000, 0.001); + const txHashFilter = BloomFilter.fromRate(10000, 0.001); + const nameHashFilter = BloomFilter.fromRate(10000, 0.001); + + // test matrix + const tests = [{ + name: 'all', + filter: null, + txs: allTXs, + // +1 for the coinbase tx + txCountCheck: (height, txs) => txs.length === allTXs[height].length + 1 + }, { + name: 'txhash', + filter: txHashFilter, + txs: txHashTXs, + // This has LOW Chance of failing because of the BloomFilter nature. + txCountCheck: (height, txs) => txs.length === txHashTXs[height].length + }, { + name: 'address', + filter: addressFilter, + txs: addressTXs, + // We can't do exact check because filter get's updated. + // (TODO: Add non-updating filter test + // issue - https://github.com/handshake-org/hsd/issues/855) + txCountCheck: (height, txs) => txs.length >= addressTXs[height].length + }, { + name: 'namehash', + filter: nameHashFilter, + txs: nameHashTXs, + // We can't do exact check because filter get's updated. + // (TODO: Add non-updating filter test + // issue - https://github.com/handshake-org/hsd/issues/855) + txCountCheck: (height, txs) => txs.length >= nameHashTXs[height].length + }]; + + before(async () => { + nodeCtx = new NodeContext(); + const {network} = nodeCtx; + + funderWallet = new MemWallet({ network }); + + await nodeCtx.open(); + + nodeCtx.on('connect', (entry, block) => { + funderWallet.addBlock(entry, block.txs); + }); + + nodeCtx.miner.addAddress(funderWallet.getReceive()); + + // Prepare addresses bloom filter. + const walletForAddrs = new MemWallet({ network }); + + for (let i = 0; i < RESCAN_DEPTH; i++) { + const addr = walletForAddrs.createReceive(); + const hash = addr.getHash(); + addressFilter.add(hash); + addresses.push(addr.getAddress().toString(network)); + } + + { + // generate 20 blocks. + const blockEvents = forEvent(nodeCtx, 'block', 20); + + for (let i = 0; i < 20; i++) { + const block = await nodeCtx.miner.mineBlock(); + await nodeCtx.chain.add(block); + } + + await blockEvents; + } + + // For 10 blocks create 3 different kind of transactions for each filter: + // 1. regular send to address. + // 2. regular send but only txhash + // 3. name open + { + const blockEvents = forEvent(nodeCtx, 'block', RESCAN_DEPTH); + + for (let i = 0; i < RESCAN_DEPTH; i++) { + const name = rules.grindName(20, nodeCtx.height, nodeCtx.network); + const nameHash = rules.hashName(name, nodeCtx.network); + nameHashFilter.add(nameHash); + names.push(name); + + const openTX = await funderWallet.sendOpen(name); + const sendTX = await funderWallet.send({ + outputs: [{ + address: addresses[i], + value: 1e4 + }] + }); + + const normalTX = await funderWallet.send({}); + const txHash = normalTX.hash(); + + allTXs[nodeCtx.height + 1] = [openTX, sendTX, normalTX]; + addressTXs[nodeCtx.height + 1] = [sendTX]; + txHashTXs[nodeCtx.height + 1] = [normalTX]; + nameHashTXs[nodeCtx.height + 1] = [openTX]; + + txHashFilter.add(txHash); + + const txEvents = forEvent(nodeCtx.mempool, 'tx', 3); + await nodeCtx.mempool.addTX(openTX.toTX()); + await nodeCtx.mempool.addTX(sendTX.toTX()); + await nodeCtx.mempool.addTX(normalTX.toTX()); + await txEvents; + + const block = await nodeCtx.miner.mineBlock(); + await nodeCtx.chain.add(block); + } + + await blockEvents; + }; + }); + + after(async () => { + await nodeCtx.close(); + await nodeCtx.destroy(); + }); + + for (const test of tests) { + it(`should rescan all blocks with ${test.name} filter`, async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + let count = 0; + + await node.scanInteractive(startHeight, test.filter, async (entry, txs) => { + assert.strictEqual(entry.height, startHeight + count); + count++; + + const testTXs = test.txs[entry.height]; + + assert(test.txCountCheck(entry.height, txs)); + const hashset = txsToTXHashes(txs); + + for (const tx of testTXs) + assert(hashset.has(tx.hash())); + + return { + type: scanActions.NEXT + }; + }); + }); + + it(`should rescan only 5 blocks and stop with ${test.name} filter`, async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + let count = 0; + + const iter = async (entry, txs) => { + assert.strictEqual(entry.height, startHeight + count); + + const testTXs = test.txs[entry.height]; + + assert(test.txCountCheck(entry.height, txs)); + const hashset = txsToTXHashes(txs); + + for (const tx of testTXs) + assert(hashset.has(tx.hash())); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.NEXT + }; + }; + + let err; + try { + await node.scanInteractive(startHeight, test.filter, iter); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'scan request aborted.'); + assert.strictEqual(count, 5); + }); + + it(`should rescan the same block 5 times with ${test.name} filter (REPEAT_SET)`, async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + let count = 0; + const iter = async (entry, txs) => { + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + assert(test.txCountCheck(entry.height, txs)); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.REPEAT_SET, + filter: test.filter + }; + }; + + let err; + try { + await node.scanInteractive(startHeight, test.filter, iter); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'scan request aborted.'); + assert.strictEqual(count, 5); + }); + + it(`should rescan the same block 5 times with ${test.name} filter (REPEAT)`, async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + let count = 0; + const iter = async (entry, txs) => { + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + assert(test.txCountCheck(entry.height, txs)); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.REPEAT + }; + }; + + let err; + try { + await node.scanInteractive(startHeight, test.filter, iter); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'scan request aborted.'); + assert.strictEqual(count, 5); + }); + } + + it('should rescan the same block with updated filters (REPEAT_SET)', async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + const filterAndTxs = tests.slice(); + let test = filterAndTxs.shift(); + + // initial run is the first filter test. + let count = 0; + const iter = async (entry, txs) => { + count++; + + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + + // we are testing against the current filter. + assert(test.txCountCheck(entry.height, txs)); + + if (filterAndTxs.length === 0) { + return { + type: scanActions.ABORT + }; + } + + // next test + test = filterAndTxs.shift(); + + return { + type: scanActions.REPEAT_SET, + filter: test.filter + }; + }; + + let err; + try { + await node.scanInteractive(startHeight, test.filter, iter); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'scan request aborted.'); + assert.strictEqual(count, tests.length); + }); + + it('should rescan the same block with updated filters (REPEAT_ADD)', async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + const filter = BloomFilter.fromRate(10000, 0.001); + const testTXs = allTXs[startHeight].slice(); + let expected = 0; + + const iter = async (entry, txs) => { + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + // May fail sometimes (BloomFilter) + assert.strictEqual(txs.length, expected); + + if (testTXs.length === 0) { + return { + type: scanActions.ABORT + }; + } + + // next test + const tx = testTXs.shift(); + const chunks = [tx.hash()]; + expected++; + + return { + type: scanActions.REPEAT_ADD, + chunks: chunks + }; + }; + + let err; + try { + await node.scanInteractive(startHeight, filter, iter); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'scan request aborted.'); + }); + + it('should rescan in parallel', async () => { + const {node} = nodeCtx; + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + const events = []; + const getIter = (counterObj) => { + return async (entry, txs) => { + assert.strictEqual(entry.height, startHeight + counterObj.count); + assert.strictEqual(txs.length, 4); + + events.push({ ...counterObj }); + counterObj.count++; + + return { + type: scanActions.NEXT + }; + }; + }; + + const counter1 = { id: 1, count: 0 }; + const counter2 = { id: 2, count: 0 }; + await Promise.all([ + node.scanInteractive(startHeight, null, getIter(counter1)), + node.scanInteractive(startHeight, null, getIter(counter2)) + ]); + + assert.strictEqual(counter1.count, 10); + assert.strictEqual(counter2.count, 10); + + // Chain gets locked per block, so we should see alternating events. + // Because they start in parallel, but id1 starts first they will be + // getting events in alternating older (first one gets lock, second waits, + // second gets lock, first waits, etc.) + for (let i = 0; i < 10; i++) { + assert.strictEqual(events[i].id, 1); + assert.strictEqual(events[i + 1].id, 2); + i++; + } + }); + + describe('HTTP', function() { + let client = null; + + beforeEach(async () => { + client = nodeCtx.nodeClient(); + + await client.open(); + }); + + afterEach(async () => { + if (client.opened) + await client.close(); + }); + + for (const test of tests) { + it(`should rescan all blocks with ${test.name} filter`, async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + let count = 0; + + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + assert.strictEqual(entry.height, startHeight + count); + count++; + + const testTXs = test.txs[entry.height]; + + assert(test.txCountCheck(entry.height, txs)); + const hashset = txsToTXHashes(txs); + + for (const tx of testTXs) + assert(hashset.has(tx.hash())); + + return { + type: scanActions.NEXT + }; + }); + + let filter = null; + + if (test.filter) + filter = test.filter.encode(); + + await client.rescanInteractive(startHeight, filter); + assert.strictEqual(count, 10); + + count = 0; + if (test.filter) + await client.setFilter(test.filter.encode()); + + await client.rescanInteractive(startHeight); + }); + + it(`should rescan only 5 blocks and stop with ${test.name} filter`, async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + let count = 0; + + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + assert.strictEqual(entry.height, startHeight + count); + + const testTXs = test.txs[entry.height]; + + assert(test.txCountCheck(entry.height, txs)); + const hashset = txsToTXHashes(txs); + + for (const tx of testTXs) + assert(hashset.has(tx.hash())); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.NEXT + }; + }); + + let aborted = false; + + client.hook('block rescan interactive abort', (message) => { + assert.strictEqual(message, 'scan request aborted.'); + aborted = true; + }); + + let filter = null; + + if (test.filter) + filter = test.filter.encode(); + + await client.rescanInteractive(startHeight, filter); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + + // rescan using socket.filter + count = 0; + aborted = false; + + if (test.filter) + await client.setFilter(test.filter.encode()); + + await client.rescanInteractive(startHeight, null); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + }); + + it(`should rescan the same block 5 times with ${test.name} filter (REPEAT_SET)`, async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + let count = 0; + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + assert(test.txCountCheck(entry.height, txs)); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.REPEAT_SET, + filter: test.filter ? test.filter.encode() : null + }; + }); + + let aborted = false; + + client.hook('block rescan interactive abort', (message) => { + assert.strictEqual(message, 'scan request aborted.'); + aborted = true; + }); + + let filter = null; + + if (test.filter) + filter = test.filter.encode(); + + await client.rescanInteractive(startHeight, filter); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + + count = 0; + aborted = false; + + if (test.filter) + await client.setFilter(test.filter.encode()); + + await client.rescanInteractive(startHeight); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + }); + + it(`should rescan the same block 5 times with ${test.name} filter (REPEAT)`, async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + let count = 0; + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + assert(test.txCountCheck(entry.height, txs)); + + count++; + + if (count === 5) { + return { + type: scanActions.ABORT + }; + } + + return { + type: scanActions.REPEAT, + filter: test.filter ? test.filter.encode() : null + }; + }); + + let aborted = false; + + client.hook('block rescan interactive abort', (message) => { + assert.strictEqual(message, 'scan request aborted.'); + aborted = true; + }); + + let filter = null; + + if (test.filter) + filter = test.filter.encode(); + + await client.rescanInteractive(startHeight, filter); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + + count = 0; + aborted = false; + + if (test.filter) + await client.setFilter(test.filter.encode()); + + await client.rescanInteractive(startHeight); + assert.strictEqual(count, 5); + assert.strictEqual(aborted, true); + }); + } + + it('should rescan the same block with update filters (REPEAT_SET)', async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + const filterAndTxs = tests.slice(); + let test = filterAndTxs.shift(); + + let count = 0; + + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + count++; + + const [entry, txs] = parseBlock(rawEntry, rawTXs); + + assert.strictEqual(entry.height, startHeight); + assert(test.txCountCheck(entry.height, txs)); + + if (filterAndTxs.length === 0) { + return { + type: scanActions.ABORT + }; + } + + test = filterAndTxs.shift(); + + return { + type: scanActions.REPEAT_SET, + filter: test.filter.encode() + }; + }); + + let aborted = false; + client.hook('block rescan interactive abort', (message) => { + assert.strictEqual(message, 'scan request aborted.'); + aborted = true; + }); + + let filter = null; + + if (test.filter) + filter = test.filter.encode(); + + await client.rescanInteractive(startHeight, filter); + assert.strictEqual(count, tests.length); + assert.strictEqual(aborted, true); + }); + + it('should rescan the same block with updated filters (REPEAT_ADD)', async () => { + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + + let testTXs = allTXs[startHeight].slice(); + let filter = BloomFilter.fromRate(10000, 0.001); + let expected = 0; + + client.hook('block rescan interactive', (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + + // we are repeating same block. + assert.strictEqual(entry.height, startHeight); + // May fail sometimes (BloomFilter) + assert.strictEqual(txs.length, expected); + + if (testTXs.length === 0) { + return { + type: scanActions.ABORT + }; + } + + // next test + const tx = testTXs.shift(); + const chunks = [tx.hash()]; + expected++; + + return { + type: scanActions.REPEAT_ADD, + chunks: chunks + }; + }); + + let aborted = false; + client.hook('block rescan interactive abort', (message) => { + assert.strictEqual(message, 'scan request aborted.'); + aborted = true; + }); + + await client.rescanInteractive(startHeight, filter.encode()); + assert.strictEqual(aborted, true); + + // Now try using client.filter + aborted = false; + filter = BloomFilter.fromRate(10000, 0.001); + testTXs = allTXs[startHeight].slice(); + expected = 0; + + await client.setFilter(filter.encode()); + await client.rescanInteractive(startHeight); + assert.strictEqual(aborted, true); + }); + + it('should rescan in parallel', async () => { + const client2 = nodeCtx.nodeClient(); + await client2.open(); + + const startHeight = nodeCtx.height - RESCAN_DEPTH + 1; + const events = []; + const counter1 = { id: 1, count: 0 }; + const counter2 = { id: 2, count: 0 }; + + const getIter = (counterObj) => { + return async (rawEntry, rawTXs) => { + const [entry, txs] = parseBlock(rawEntry, rawTXs); + assert.strictEqual(entry.height, startHeight + counterObj.count); + assert.strictEqual(txs.length, 4); + + events.push({ ...counterObj }); + counterObj.count++; + + return { + type: scanActions.NEXT + }; + }; + }; + + client.hook('block rescan interactive', getIter(counter1)); + client2.hook('block rescan interactive', getIter(counter2)); + + await Promise.all([ + client.rescanInteractive(startHeight), + client2.rescanInteractive(startHeight) + ]); + + assert.strictEqual(counter1.count, 10); + assert.strictEqual(counter2.count, 10); + + // Chain gets locked per block, so we should see alternating events. + // Because they start in parallel, but id1 starts first they will be + // getting events in alternating older (first one gets lock, second waits, + // second gets lock, first waits, etc.) + for (let i = 0; i < 10; i++) { + assert.strictEqual(events[i].id, 1); + assert.strictEqual(events[i + 1].id, 2); + i++; + } + }); + + // Make sure the client closing does not cause the chain locker to get + // indefinitely locked. (https://github.com/bcoin-org/bsock/pull/11) + it('should stop rescan when client closes', async () => { + const client2 = nodeCtx.nodeClient(); + + const addr = funderWallet.getAddress().toString(nodeCtx.network); + + // Client does not need rescan hooks, because we make + // sure that the rescan hooks are never actually called. + // Client closes before they are called. + // We simulate this by acquiring chain lock before we + // call rescan and then closing the client. + const unlock = await nodeCtx.chain.locker.lock(); + const rescan = client.rescanInteractive(0); + let err = null; + rescan.catch(e => err = e); + + // make sure call reaches the server. + await sleep(50); + await client.close(); + try { + await rescan; + } catch (e) { + err = e; + } + + assert(err); + assert(err.message, 'Job timed out.'); + unlock(); + + // Make sure lock was unlocked. + // w/o bsock update this will fail with timeout. + await client2.execute('generatetoaddress', [1, addr]); + }); + }); +}); + +function txsToTXHashes(txs) { + return new BufferSet(txs.map(tx => tx.hash())); +} + +function parseEntry(data) { + // 32 hash + // 4 height + // 4 nonce + // 8 time + // 32 prev + // 32 tree + // 24 extranonce + // 32 reserved + // 32 witness + // 32 merkle + // 4 version + // 4 bits + // 32 mask + // 32 chainwork + // 304 TOTAL + + assert(Buffer.isBuffer(data)); + // Just enough to read the three data below + assert(data.length >= 44); + + return { + hash: data.slice(0, 32), + height: data.readUInt32LE(32), + time: data.readUInt32LE(40) + }; +} + +function parseBlock(entry, txs) { + const block = parseEntry(entry); + const out = []; + + for (const tx of txs) + out.push(TX.decode(tx)); + + return [block, out]; +} diff --git a/test/util/chain.js b/test/util/chain.js index 6e338d36c..36c1bcdea 100644 --- a/test/util/chain.js +++ b/test/util/chain.js @@ -116,13 +116,32 @@ chainUtils.syncChain = async (fromChain, toChain, startHeight) => { return endHeight - startHeight; }; -chainUtils.chainTreeHas = async (chain, name) => { +chainUtils.mineBlock = async (chainObj, mtxs) => { + const tip = chainObj.chain.tip; + const job = await chainObj.miner.createJob(tip); + + if (mtxs) { + for (const mtx of mtxs) { + const [tx, view] = mtx.commit(); + + job.addTX(tx, view); + } + } + + job.refresh(); + + const block = await job.mineAsync(); + const entry = await chainObj.chain.add(block); + return { block, entry }; +}; + +chainUtils.chainTreeHasName = async (chain, name) => { assert(!chain.options.spv); const hash = rules.hashName(name); return await chain.db.tree.get(hash) != null; }; -chainUtils.chainTxnHas = async (chain, name) => { +chainUtils.chainTxnHasName = async (chain, name) => { assert(!chain.options.spv); const hash = rules.hashName(name); return await chain.db.txn.get(hash) != null; diff --git a/test/util/node.js b/test/util/node.js new file mode 100644 index 000000000..fd11010cd --- /dev/null +++ b/test/util/node.js @@ -0,0 +1,311 @@ +'use strict'; + +const assert = require('bsert'); +const common = require('./common'); +const fs = require('bfile'); +const SPVNode = require('../../lib/node/spvnode'); +const FullNode = require('../../lib/node/fullnode'); +const plugin = require('../../lib/wallet/plugin'); +const Network = require('../../lib/protocol/network'); +const {NodeClient, WalletClient} = require('../../lib/client'); +const Logger = require('blgr'); + +class NodeContext { + constructor(options = {}) { + this.name = 'node-test'; + this.options = {}; + this.node = null; + this.opened = false; + this.logger = new Logger({ + console: true, + filename: null, + level: 'none' + }); + + this.nclient = null; + this.wclient = null; + + this.clients = []; + + this.fromOptions(options); + } + + fromOptions(options) { + const fnodeOptions = { + ...options, + memory: true, + workers: true, + network: 'regtest', + listen: false, + wallet: false, + spv: false, + logger: this.logger + }; + + if (options.network != null) + fnodeOptions.network = Network.get(options.network).type; + + if (options.name != null) + fnodeOptions.name = options.name; + + if (options.listen != null) { + assert(typeof options.listen === 'boolean'); + fnodeOptions.listen = options.listen; + } + + if (options.prefix != null) { + fnodeOptions.prefix = this.prefix; + fnodeOptions.memory = false; + } + + if (options.memory != null) { + assert(!fnodeOptions.prefix, 'Can not set prefix with memory.'); + fnodeOptions.memory = options.memory; + } + + if (!this.memory && !this.prefix) + fnodeOptions.prefix = common.testdir(this.name); + + if (options.wallet != null) + fnodeOptions.wallet = options.wallet; + + if (options.apiKey != null) { + assert(typeof options.apiKey === 'string'); + fnodeOptions.apiKey = options.apiKey; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + fnodeOptions.spv = options.spv; + } + + if (options.httpPort != null) { + assert(typeof options.httpPort === 'number'); + fnodeOptions.httpPort = options.httpPort; + } + + if (options.indexTX != null) { + assert(typeof options.indexTX === 'boolean'); + fnodeOptions.indexTX = options.indexTX; + } + + if (options.indexAddress != null) { + assert(typeof options.indexAddress === 'boolean'); + fnodeOptions.indexAddress = options.indexAddress; + } + + if (options.prune != null) { + assert(typeof options.prune === 'boolean'); + fnodeOptions.prune = options.prune; + } + + if (options.compactOnInit != null) { + assert(typeof options.compactOnInit === 'boolean'); + fnodeOptions.compactOnInit = options.compactOnInit; + } + + if (options.compactTreeInitInterval != null) { + assert(typeof options.compactTreeInitInterval === 'number'); + fnodeOptions.compactTreeInitInterval = options.compactTreeInitInterval; + } + + if (fnodeOptions.spv) + this.node = new SPVNode(fnodeOptions); + else + this.node = new FullNode(fnodeOptions); + + if (options.timeout != null) + fnodeOptions.timeout = options.timeout; + + if (fnodeOptions.wallet) + this.node.use(plugin); + + this.nclient = new NodeClient({ + timeout: fnodeOptions.timeout, + apiKey: fnodeOptions.apiKey, + port: fnodeOptions.httpPort || this.node.network.rpcPort + }); + + this.clients.push(this.nclient); + + if (fnodeOptions.wallet) { + this.wclient = new WalletClient({ + timeout: fnodeOptions.timeout, + port: this.node.network.walletPort + }); + + this.clients.push(this.wclient); + } + + this.options = fnodeOptions; + } + + get network() { + return this.node.network; + } + + get miner() { + return this.node.miner; + } + + get mempool() { + return this.node.mempool; + } + + get chain() { + return this.node.chain; + } + + get height() { + return this.chain.tip.height; + } + + get wdb() { + return this.node.get('walletdb').wdb; + } + + /* + * Event Listeners wrappers + */ + + on(event, listener) { + this.node.on(event, listener); + } + + once(event, listener) { + this.node.once(event, listener); + } + + addListener(event, listener) { + this.node.addListener(event, listener); + } + + removeListener(event, listener) { + this.node.removeListener(event, listener); + } + + removeAllListeners(event) { + this.node.removeAllListeners(event); + } + + /* + * Life Cycle + */ + + async open() { + if (this.prefix) + await fs.mkdirp(this.prefix); + + await this.node.ensure(); + await this.node.open(); + await this.node.connect(); + this.node.startSync(); + + if (this.wclient) + await this.wclient.open(); + + await this.nclient.open(); + + this.opened = true; + } + + async close() { + if (!this.opened) + return; + + const close = common.forEvent(this.node, 'close'); + const closeClients = []; + + for (const client of this.clients) { + if (client.opened) + closeClients.push(client.close()); + } + + await Promise.all(closeClients); + await this.node.close(); + await close; + + this.opened = false; + } + + async destroy() { + if (this.prefix) + await fs.rimraf(this.prefix); + } + + /* + * Helpers + */ + + enableLogging() { + this.logger.setLevel('debug'); + } + + disableLogging() { + this.logger.setLevel('none'); + } + + /** + * Execute an RPC using the node client. + * @param {String} method - RPC method + * @param {Array} params - method parameters + * @returns {Promise} - Returns a two item array with the + * RPC's return value or null as the first item and an error or + * null as the second item. + */ + + async nrpc(method, params) { + return this.nclient.execute(method, params); + } + + /** + * Execute an RPC using the wallet client. + * @param {String} method - RPC method + * @param {Array} params - method parameters + * @returns {Promise} - Returns a two item array with the RPC's return value + * or null as the first item and an error or null as the second item. + */ + + async wrpc(method, params) { + return this.wclient.execute(method, params); + }; + + /** + * Create new client + * @param {Object} [options] + * @returns {NodeClient} + */ + + nodeClient(options = {}) { + const client = new NodeClient({ + timeout: this.options.timeout, + apiKey: this.options.apiKey, + port: this.options.httpPort || this.network.rpcPort, + ...options + }); + + this.clients.push(client); + + return client; + } + + /** + * Create new wallet client. + * @param {Object} [options] + * @returns {WalletClient} + */ + + walletClient(options = {}) { + const client = new WalletClient({ + timeout: this.options.timeout, + port: this.network.walletPort, + ...options + }); + + this.clients.push(client); + + return client; + } +} + +module.exports = NodeContext; diff --git a/test/util/node-context.js b/test/util/nodes-context.js similarity index 97% rename from test/util/node-context.js rename to test/util/nodes-context.js index 76a1d97a5..1de3b98a7 100644 --- a/test/util/node-context.js +++ b/test/util/nodes-context.js @@ -5,7 +5,7 @@ const FullNode = require('../../lib/node/fullnode'); const Network = require('../../lib/protocol/network'); const Logger = require('blgr'); -class NodeContext { +class NodesContext { constructor(network, size) { this.network = Network.get(network); this.size = size || 4; @@ -120,4 +120,4 @@ class NodeContext { } } -module.exports = NodeContext; +module.exports = NodesContext;