From 29625eb31689b75914d7243664bacfe7a829b540 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Tue, 24 Oct 2023 18:53:46 +0400 Subject: [PATCH] walletdb: Add guard for the rescan in the add block. --- lib/wallet/walletdb.js | 31 +++-- test/wallet-balance-test.js | 4 +- test/wallet-namestate-rescan-test.js | 188 ++++++++------------------- test/wallet-rescan-test.js | 81 ++++++++++++ 4 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 test/wallet-rescan-test.js diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index e0736bd59..135eb09ba 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -143,6 +143,13 @@ class WalletDB extends EventEmitter { }); this.client.bind('block connect', async (entry, txs) => { + // If we are rescanning or doing initial sync we ignore + // block connect events. This avoids deadlocks when using + // nodeclient, but also skips unnecessary addBlock calls + // that would just repeat after the txLock is unlocked. + if (this.rescanning) + return; + try { await this.addBlock(entry, txs); } catch (e) { @@ -385,6 +392,7 @@ class WalletDB extends EventEmitter { async syncNode() { const unlock = await this.txLock.lock(); + this.rescanning = true; try { this.logger.info('Resyncing from server...'); await this.syncInitState(); @@ -392,6 +400,7 @@ class WalletDB extends EventEmitter { await this.syncChain(); await this.resend(); } finally { + this.rescanning = false; unlock(); } } @@ -457,6 +466,7 @@ class WalletDB extends EventEmitter { /** * Connect and sync with the chain server. + * Part of syncNode. * @private * @returns {Promise} */ @@ -477,11 +487,13 @@ class WalletDB extends EventEmitter { height -= 1; } + // syncNode sets the rescanning to true. return this.scan(height); } /** * Rescan blockchain from a given height. + * Needs this.rescanning = true to be set from the caller. * @private * @param {Number?} height * @returns {Promise} @@ -501,12 +513,7 @@ class WalletDB extends EventEmitter { const tip = await this.getTip(); - try { - this.rescanning = true; - await this.client.rescan(tip.hash); - } finally { - this.rescanning = false; - } + return this.client.rescan(tip.hash); } /** @@ -600,6 +607,7 @@ class WalletDB extends EventEmitter { async rescan(height) { const unlock = await this.txLock.lock(); + try { return await this._rescan(height); } finally { @@ -615,7 +623,13 @@ class WalletDB extends EventEmitter { */ async _rescan(height) { - return this.scan(height); + this.rescanning = true; + + try { + return await this.scan(height); + } finally { + this.rescanning = false; + } } /** @@ -2237,6 +2251,7 @@ class WalletDB extends EventEmitter { async addBlock(entry, txs) { const unlock = await this.txLock.lock(); + try { return await this._addBlock(entry, txs); } finally { @@ -2274,7 +2289,7 @@ class WalletDB extends EventEmitter { // processed (in the case of a crash). this.logger.warning('Already saw WalletDB block (%d).', tip.height); } else if (tip.height !== this.state.height + 1) { - await this.scan(this.state.height); + await this._rescan(this.state.height); return 0; } diff --git a/test/wallet-balance-test.js b/test/wallet-balance-test.js index 403bbcd8d..66b5902f5 100644 --- a/test/wallet-balance-test.js +++ b/test/wallet-balance-test.js @@ -438,7 +438,7 @@ describe('Wallet Balance', function() { await discoverFn(wallet, ahead, opts); // Final look at full picture. - await wdb.scan(chain.tip.height - 1); + await wdb.rescan(chain.tip.height - 1); await checks.blockConfirmCheck(wallet, ahead, opts); if (discoverAt === BEFORE_BLOCK_UNCONFIRM) @@ -449,7 +449,7 @@ describe('Wallet Balance', function() { await checks.blockUnconfirmCheck(wallet, ahead, opts); // Clean up wallet. - await wdb.scan(chain.tip.height - 1); + await wdb.rescan(chain.tip.height - 1); await checks.blockFinalConfirmCheck(wallet, ahead, opts); }; }; diff --git a/test/wallet-namestate-rescan-test.js b/test/wallet-namestate-rescan-test.js index 83adf89f2..fbb0a32a3 100644 --- a/test/wallet-namestate-rescan-test.js +++ b/test/wallet-namestate-rescan-test.js @@ -21,77 +21,81 @@ const { const GNAME_SIZE = 10; describe('Wallet rescan with namestate transitions', function() { - describe('Only sends OPEN', function() { - // Bob runs a full node with wallet plugin - const node = new FullNode({ + let node, wdb; + let alice, aliceAddr; + let bob, bobAddr; + + async function mineBlocks(n, addr) { + addr = addr ? addr : new Address().toString('regtest'); + const blocks = []; + for (let i = 0; i < n; i++) { + const block = await node.miner.mineBlock(null, addr); + await node.chain.add(block); + blocks.push(block); + } + + return blocks; + } + + async function sendTXs() { + const aliceTX = await alice.send({ + outputs: [{ + address: aliceAddr, + value: 20000 + }] + }); + alice.addTX(aliceTX.toTX()); + await node.mempool.addTX(aliceTX.toTX()); + await bob.send({ + outputs: [{ + address: bobAddr, + value: 20000 + }] + }); + } + + const beforeAll = async () => { + node = new FullNode({ network: network.type, memory: true, plugins: [require('../lib/wallet/plugin')] }); + node.on('error', (err) => { assert(false, err); }); - const {wdb} = node.require('walletdb'); - let bob, bobAddr; + wdb = node.require('walletdb').wdb; - // Alice is some other wallet on the network - const alice = new MemWallet({ network }); - const aliceAddr = alice.getAddress(); + alice = new MemWallet({ network }); + aliceAddr = alice.getAddress(); // Connect MemWallet to chain as minimally as possible node.chain.on('connect', (entry, block) => { alice.addBlock(entry, block.txs); }); + alice.getNameStatus = async (nameHash) => { assert(Buffer.isBuffer(nameHash)); const height = node.chain.height + 1; return node.chain.db.getNameStatus(nameHash, height); }; - const NAME = rules.grindName(GNAME_SIZE, 4, network); - - // Hash of the FINALIZE transaction - let aliceFinalizeHash; - - async function mineBlocks(n, addr) { - addr = addr ? addr : new Address().toString('regtest'); - const blocks = []; - for (let i = 0; i < n; i++) { - const block = await node.miner.mineBlock(null, addr); - await node.chain.add(block); - blocks.push(block); - } + await node.open(); + bob = await wdb.create(); + bobAddr = await bob.receiveAddress(); + }; - return blocks; - } + const afterAll = async () => { + await node.close(); + }; - async function sendTXs() { - const aliceTX = await alice.send({ - outputs: [{ - address: aliceAddr, - value: 20000 - }] - }); - alice.addTX(aliceTX.toTX()); - await node.mempool.addTX(aliceTX.toTX()); - await bob.send({ - outputs: [{ - address: bobAddr, - value: 20000 - }] - }); - } - - before(async () => { - await node.open(); - bob = await wdb.create(); - bobAddr = await bob.receiveAddress(); - }); + describe('Only sends OPEN', function() { + const NAME = rules.grindName(GNAME_SIZE, 4, network); + let aliceFinalizeHash; - after(async () => { - await node.close(); - }); + before(beforeAll); + after(afterAll); it('should fund wallets', async () => { const blocks = 10; @@ -274,78 +278,12 @@ describe('Wallet rescan with namestate transitions', function() { }); describe('Bids, loses, shallow rescan', function() { - // Bob runs a full node with wallet plugin - const node = new FullNode({ - network: network.type, - memory: true, - plugins: [require('../lib/wallet/plugin')] - }); - node.on('error', (err) => { - assert(false, err); - }); - - const {wdb} = node.require('walletdb'); - let bob, bobAddr; - - // Alice is some other wallet on the network - const alice = new MemWallet({ network }); - const aliceAddr = alice.getAddress(); - - // Connect MemWallet to chain as minimally as possible - node.chain.on('connect', (entry, block) => { - alice.addBlock(entry, block.txs); - }); - alice.getNameStatus = async (nameHash) => { - assert(Buffer.isBuffer(nameHash)); - const height = node.chain.height + 1; - return node.chain.db.getNameStatus(nameHash, height); - }; - const NAME = rules.grindName(GNAME_SIZE, 4, network); - - // Block that confirmed the bids - let bidBlockHash; - // Hash of the FINALIZE transaction let aliceFinalizeHash; + let bidBlockHash; - async function mineBlocks(n, addr) { - addr = addr ? addr : new Address().toString('regtest'); - const blocks = []; - for (let i = 0; i < n; i++) { - const block = await node.miner.mineBlock(null, addr); - await node.chain.add(block); - blocks.push(block); - } - - return blocks; - } - - async function sendTXs() { - const aliceTX = await alice.send({ - outputs: [{ - address: aliceAddr, - value: 20000 - }] - }); - alice.addTX(aliceTX.toTX()); - await node.mempool.addTX(aliceTX.toTX()); - await bob.send({ - outputs: [{ - address: bobAddr, - value: 20000 - }] - }); - } - - before(async () => { - await node.open(); - bob = await wdb.create(); - bobAddr = await bob.receiveAddress(); - }); - - after(async () => { - await node.close(); - }); + before(beforeAll); + after(afterAll); it('should fund wallets', async () => { const blocks = 10; @@ -562,26 +500,14 @@ describe('Wallet rescan with namestate transitions', function() { }); describe('Restore from seed', function() { - const node = new FullNode({ - network: network.type, - memory: true, - plugins: [require('../lib/wallet/plugin')] - }); - - const {wdb} = node.require('walletdb'); let wallet1, wallet2, wallet3; let addr1; let heightBeforeReveal; const name = rules.grindName(4, 4, network); - before(async () => { - await node.open(); - }); - - after(async () => { - await node.close(); - }); + before(beforeAll); + after(afterAll); it('should create and fund wallet 1', async () => { wallet1 = await wdb.create(); diff --git a/test/wallet-rescan-test.js b/test/wallet-rescan-test.js new file mode 100644 index 000000000..3e0b997d8 --- /dev/null +++ b/test/wallet-rescan-test.js @@ -0,0 +1,81 @@ +'use strict'; + +const assert = require('bsert'); +const FullNode = require('../lib/node/fullnode'); +const WalletPlugin = require('../lib/wallet/plugin'); +const Network = require('../lib/protocol/network'); +const {forEvent} = require('./util/common'); + +// TODO: Rewrite using util/node from the interactive rescan test. +// TODO: Add the standalone Wallet variation. +// TODO: Add initial rescan test. + +describe('Wallet rescan', function() { + const network = Network.get('regtest'); + + let node, wdb; + + const beforeAll = async () => { + node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [WalletPlugin] + }); + + node.on('error', (err) => { + assert(false, err); + }); + + wdb = node.require('walletdb').wdb; + const wdbSynced = forEvent(wdb, 'sync done'); + + await node.open(); + await wdbSynced; + }; + + const afterAll = async () => { + await node.close(); + node = null; + wdb = null; + }; + + describe('Deadlock', function() { + let address; + + before(async () => { + await beforeAll(); + + address = await wdb.primary.receiveAddress(); + }); + + after(afterAll); + + it('should generate 10 blocks', async () => { + await node.rpc.generateToAddress([10, address.toString(network)]); + }); + + it('should rescan when receiving a block', async () => { + const preTip = await wdb.getTip(); + + await Promise.all([ + node.rpc.generateToAddress([1, address.toString(network)]), + wdb.rescan(0) + ]); + + const wdbTip = await wdb.getTip(); + assert.strictEqual(wdbTip.height, preTip.height + 1); + }); + + it('should rescan when receiving a block', async () => { + const preTip = await wdb.getTip(); + + await Promise.all([ + wdb.rescan(0), + node.rpc.generateToAddress([1, address.toString(network)]) + ]); + + const tip = await wdb.getTip(); + assert.strictEqual(tip.height, preTip.height + 1); + }); + }); +});