diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 30a7112b6..d3c86013b 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -7,7 +7,6 @@ 'use strict'; const assert = require('bsert'); -const {Lock} = require('bmutex'); const NameState = require('../covenants/namestate'); const Chain = require('../blockchain/chain'); const Pool = require('../net/pool'); @@ -113,10 +112,6 @@ class SPVNode extends Node { noUnbound: this.config.bool('rs-no-unbound') }); - this.rescanJob = null; - this.scanLock = new Lock(); - this.watchLock = new Lock(); - this.init(); } @@ -134,9 +129,6 @@ class SPVNode extends Node { this.http.on('error', err => this.error(err)); this.pool.on('tx', (tx) => { - if (this.rescanJob) - return; - this.emit('tx', tx); }); @@ -145,15 +137,6 @@ class SPVNode extends Node { }); this.chain.on('connect', async (entry, block) => { - if (this.rescanJob) { - try { - await this.watchBlock(entry, block); - } catch (e) { - this.error(e); - } - return; - } - this.emit('connect', entry, block); }); @@ -221,64 +204,11 @@ class SPVNode extends Node { * Scan for any missed transactions. * Note that this will replay the blockchain sync. * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. - * @returns {Promise} - */ - - async scan(start, filter, iter) { - const unlock = await this.scanLock.lock(); - const height = this.chain.height; - - try { - await this.chain.replay(start); - - if (this.chain.height < height) { - // We need to somehow defer this. - // await this.connect(); - // this.startSync(); - // await this.watchUntil(height, iter); - } - } finally { - unlock(); - } - } - - /** - * Watch the blockchain until a certain height. - * @param {Number} height - * @param {Function} iter - * @returns {Promise} - */ - - watchUntil(height, iter) { - return new Promise((resolve, reject) => { - this.rescanJob = new RescanJob(resolve, reject, height, iter); - }); - } - - /** - * Handled watched block. - * @param {ChainEntry} entry - * @param {MerkleBlock} block * @returns {Promise} */ - async watchBlock(entry, block) { - const unlock = await this.watchLock.lock(); - try { - if (entry.height < this.rescanJob.height) { - await this.rescanJob.iter(entry, block.txs); - return; - } - this.rescanJob.resolve(); - this.rescanJob = null; - } catch (e) { - this.rescanJob.reject(e); - this.rescanJob = null; - } finally { - unlock(); - } + async scan(start) { + throw new Error('Not implemented.'); } /** @@ -428,19 +358,6 @@ class SPVNode extends Node { } } -/* - * Helpers - */ - -class RescanJob { - constructor(resolve, reject, height, iter) { - this.resolve = resolve; - this.reject = reject; - this.height = height; - this.iter = iter; - } -} - /* * Expose */ diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 32f1f26df..d9c7b57a8 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -1297,7 +1297,7 @@ class HTTP extends Server { if (!this.channel(name) && !this.channel('w:*')) return; - const json = details.getJSON(this.network, this.wdb.height); + const json = details.getJSON(this.network, this.wdb.liveHeight()); if (this.channel(name)) this.to(name, event, wallet.id, json); diff --git a/lib/wallet/node.js b/lib/wallet/node.js index 6665fc4a1..8c8450dc6 100644 --- a/lib/wallet/node.js +++ b/lib/wallet/node.js @@ -49,7 +49,6 @@ class WalletNode extends Node { memory: this.config.bool('memory'), maxFiles: this.config.uint('max-files'), cacheSize: this.config.mb('cache-size'), - checkpoints: this.config.bool('checkpoints'), wipeNoReally: this.config.bool('wipe-no-really'), spv: this.config.bool('spv') }); diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index c5a7ab115..bae7bd312 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -36,18 +36,18 @@ class NodeClient extends AsyncEmitter { */ init() { - this.node.on('connect', (entry, block) => { + this.node.chain.on('connect', async (entry, block) => { if (!this.opened) return; - this.emit('block connect', entry, block.txs); + await this.emitAsync('block connect', entry, block.txs); }); - this.node.on('disconnect', (entry, block) => { + this.node.chain.on('disconnect', async (entry, block) => { if (!this.opened) return; - this.emit('block disconnect', entry); + await this.emitAsync('block disconnect', entry); }); this.node.on('tx', (tx) => { @@ -217,6 +217,9 @@ class NodeClient extends AsyncEmitter { */ async rescan(start) { + if (this.node.spv) + return this.node.chain.reset(start); + return this.node.chain.scan(start, this.filter, (entry, txs) => { return this.emitAsync('block rescan', entry, txs); }); diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 93f54f6c1..c0bdb585e 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -50,8 +50,6 @@ class Plugin extends EventEmitter { memory: this.config.bool('memory', node.memory), maxFiles: this.config.uint('max-files'), cacheSize: this.config.mb('cache-size'), - witness: this.config.bool('witness'), - checkpoints: this.config.bool('checkpoints'), wipeNoReally: this.config.bool('wipe-no-really'), spv: node.spv }); diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 746ff8679..e00ab1531 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -62,6 +62,7 @@ class WalletDB extends EventEmitter { this.primary = null; this.state = new ChainState(); + this.confirming = false; this.height = 0; this.wallets = new Map(); this.depth = 0; @@ -1657,6 +1658,22 @@ class WalletDB extends EventEmitter { this.height = state.height; } + /** + * Will return the current height and will increment + * to the current height of a block currently being + * added to the wallet. + * @returns {Number} + */ + + liveHeight() { + let height = this.height; + + if (this.confirming) + height += 1; + + return height; + } + /** * Mark current state. * @param {BlockMeta} block @@ -2062,19 +2079,23 @@ class WalletDB extends EventEmitter { return 0; } - // Sync the state to the new tip. - await this.setTip(tip); - - if (this.options.checkpoints && !this.state.marked) { - if (tip.height <= this.network.lastCheckpoint) - return 0; - } - let total = 0; - for (const tx of txs) { - if (await this._addTX(tx, tip)) - total += 1; + try { + // We set the state as confirming so that + // anything that uses the current height can + // increment by one until the block is fully + // added and the height is updated. + this.confirming = true; + for (const tx of txs) { + if (await this._addTX(tx, tip)) + total += 1; + } + + // Sync the state to the new tip. + await this.setTip(tip); + } finally { + this.confirming = false; } if (total > 0) { @@ -2299,7 +2320,6 @@ class WalletOptions { this.compression = true; this.spv = false; - this.checkpoints = false; this.wipeNoReally = false; if (options) @@ -2373,11 +2393,6 @@ class WalletOptions { this.spv = options.spv; } - if (options.checkpoints != null) { - assert(typeof options.checkpoints === 'boolean'); - this.checkpoints = options.checkpoints; - } - if (options.wipeNoReally != null) { assert(typeof options.wipeNoReally === 'boolean'); this.wipeNoReally = options.wipeNoReally; diff --git a/test/wallet-test.js b/test/wallet-test.js index c9ca86e2d..f5c43b99b 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -5,11 +5,13 @@ 'use strict'; const assert = require('bsert'); +const {WalletClient} = require('hs-client'); const consensus = require('../lib/protocol/consensus'); const Network = require('../lib/protocol/network'); const util = require('../lib/utils/util'); const blake2b = require('bcrypto/lib/blake2b'); const random = require('bcrypto/lib/random'); +const FullNode = require('../lib/node/fullnode'); const WalletDB = require('../lib/wallet/walletdb'); const WorkerPool = require('../lib/workers/workerpool'); const Address = require('../lib/primitives/address'); @@ -22,6 +24,7 @@ const Script = require('../lib/script/script'); const policy = require('../lib/protocol/policy'); const HDPrivateKey = require('../lib/hd/private'); const Wallet = require('../lib/wallet/wallet'); +const {forValue} = require('./util/common'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -1819,6 +1822,70 @@ describe('Wallet', function() { }); }); + describe('Corruption', function() { + let workers = null; + let wdb = null; + + beforeEach(async () => { + workers = new WorkerPool({ + enabled: true, + size: 2 + }); + + wdb = new WalletDB({ workers }); + await workers.open(); + await wdb.open(); + }); + + afterEach(async () => { + await wdb.close(); + await workers.close(); + }); + + it('should not write tip with error in txs', async () => { + const alice = await wdb.create(); + const addr = await alice.receiveAddress(); + + const fund = new MTX(); + fund.addInput(dummyInput()); + fund.addOutput(addr, 5460 * 10); + + wdb._addTX = async () => { + throw new Error('Some assertion.'); + }; + + await assert.rejects(async () => { + await wdb.addBlock(nextBlock(wdb), [fund.toTX()]); + }, { + message: 'Some assertion.' + }); + + assert.equal(wdb.height, 0); + + const bal = await alice.getBalance(); + assert.equal(bal.confirmed, 0); + assert.equal(bal.unconfirmed, 0); + }); + + it('should write tip without error in txs', async () => { + const alice = await wdb.create(); + const addr = await alice.receiveAddress(); + + const fund = new MTX(); + fund.addInput(dummyInput()); + const amount = 5460 * 10; + fund.addOutput(addr, amount); + + await wdb.addBlock(nextBlock(wdb), [fund.toTX()]); + + assert.equal(wdb.height, 1); + + const bal = await alice.getBalance(); + assert.equal(bal.confirmed, amount); + assert.equal(bal.unconfirmed, amount); + }); + }); + describe('TXDB locked balance', function() { const network = Network.get('regtest'); const workers = new WorkerPool({ enabled }); @@ -2112,4 +2179,104 @@ describe('Wallet', function() { assert.strictEqual(bal.clocked, value); }); }); + + describe('Node Integration', function() { + const ports = {p2p: 49331, node: 49332, wallet: 49333}; + let node, chain, miner, wdb = null; + + beforeEach(async () => { + node = new FullNode({ + memory: true, + network: 'regtest', + workers: true, + workersSize: 2, + plugins: [require('../lib/wallet/plugin')], + port: ports.p2p, + httpPort: ports.node, + env: { + 'HSD_WALLET_HTTP_PORT': ports.wallet.toString() + } + }); + + chain = node.chain; + miner = node.miner; + wdb = node.require('walletdb').wdb; + await node.open(); + }); + + afterEach(async () => { + await node.close(); + }); + + async function mineBlock(tip) { + const job = await miner.createJob(tip); + const block = await job.mineAsync(); + return chain.add(block); + } + + it('should not stack in-memory block queue (oom)', async () => { + let height = 0; + + const addBlock = wdb.addBlock.bind(wdb); + wdb.addBlock = async (entry, txs) => { + await new Promise(resolve => setTimeout(resolve, 100)); + await addBlock(entry, txs); + }; + + async function raceForward() { + await mineBlock(); + + await forValue(node.chain, 'height', height + 1); + assert.equal(wdb.height, height + 1); + + height += 1; + } + + for (let i = 0; i < 10; i++) + await raceForward(); + }); + + it('should emit details with correct confirmation', async () => { + const wclient = new WalletClient({port: ports.wallet}); + await wclient.open(); + + const info = await wclient.createWallet('test'); + const wallet = wclient.wallet('test', info.token); + await wallet.open(); + + const acct = await wallet.getAccount('default'); + const waddr = acct.receiveAddress; + + miner.addresses.length = 0; + miner.addAddress(waddr); + + let txCount = 0; + let txConfirmedCount = 0; + let confirmedCount = 0; + + wallet.on('tx', (details) => { + if (details.confirmations === 1) + txConfirmedCount += 1; + else if (details.confirmations === 0) + txCount += 1; + }); + + wallet.on('confirmed', (details) => { + assert.equal(details.confirmations, 1); + confirmedCount += 1; + }); + + for (let i = 0; i < 101; i++) + await mineBlock(); + + await wallet.send({outputs: [{address: waddr, value: 1 * 1e8}]}); + await mineBlock(); + + await wclient.close(); + + assert.equal(txConfirmedCount, 102); + assert.equal(txCount, 1); + assert.equal(confirmedCount, 1); + }); + }); });