From 949a03eee20601451a45b0f4a68fc90b6860857b Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Thu, 2 Nov 2023 15:54:33 +0400 Subject: [PATCH 1/2] wallet-test: Add test case for the chainstate. --- lib/wallet/walletdb.js | 8 +- test/util/wallet.js | 61 ++++ test/wallet-chainstate-test.js | 567 +++++++++++++++++++++++++++++++++ test/wallet-test.js | 62 +--- 4 files changed, 642 insertions(+), 56 deletions(-) create mode 100644 test/util/wallet.js create mode 100644 test/wallet-chainstate-test.js diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 135eb09ba..2c27d5711 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -2142,7 +2142,7 @@ class WalletDB extends EventEmitter { /** * Get a wallet block meta. * @param {Hash} hash - * @returns {Promise} + * @returns {Promise} */ async getBlock(height) { @@ -2157,7 +2157,7 @@ class WalletDB extends EventEmitter { /** * Get wallet tip. * @param {Hash} hash - * @returns {Promise} + * @returns {Promise} */ async getTip() { @@ -2301,6 +2301,7 @@ class WalletDB extends EventEmitter { // 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)) { walletTxs.push(tx); @@ -2453,6 +2454,9 @@ class WalletDB extends EventEmitter { if (!wids) return null; + // This is better place than _addBlock because here above check + // makes sure we really own the txs and it was not a false positive + // from the filter. if (block && !this.state.marked) await this.markState(block); diff --git a/test/util/wallet.js b/test/util/wallet.js new file mode 100644 index 000000000..812ea34de --- /dev/null +++ b/test/util/wallet.js @@ -0,0 +1,61 @@ +'use strict'; + +const blake2b = require('bcrypto/lib/blake2b'); +const random = require('bcrypto/lib/random'); +const Block = require('../../lib/primitives/block'); +const ChainEntry = require('../../lib/blockchain/chainentry'); +const Input = require('../../lib/primitives/input'); +const Outpoint = require('../../lib/primitives/outpoint'); + +const walletUtils = exports; + +walletUtils.fakeBlock = (height) => { + const prev = blake2b.digest(fromU32((height - 1) >>> 0)); + const hash = blake2b.digest(fromU32(height >>> 0)); + const root = blake2b.digest(fromU32((height | 0x80000000) >>> 0)); + + return { + hash: hash, + prevBlock: prev, + merkleRoot: root, + time: 500000000 + (height * (10 * 60)), + bits: 0, + nonce: 0, + height: height, + version: 0, + witnessRoot: Buffer.alloc(32), + treeRoot: Buffer.alloc(32), + reservedRoot: Buffer.alloc(32), + extraNonce: Buffer.alloc(24), + mask: Buffer.alloc(32) + }; +}; + +walletUtils.dummyInput = () => { + const hash = random.randomBytes(32); + return Input.fromOutpoint(new Outpoint(hash, 0)); +}; + +walletUtils.nextBlock = (wdb) => { + return walletUtils.fakeBlock(wdb.state.height + 1); +}; + +walletUtils.curBlock = (wdb) => { + return walletUtils.fakeBlock(wdb.state.height); +}; + +walletUtils.nextEntry = (wdb) => { + const cur = walletUtils.curEntry(wdb); + const next = new Block(walletUtils.nextBlock(wdb)); + return ChainEntry.fromBlock(next, cur); +}; + +walletUtils.curEntry = (wdb) => { + return new ChainEntry(walletUtils.curBlock(wdb)); +}; + +function fromU32(num) { + const data = Buffer.allocUnsafe(4); + data.writeUInt32LE(num, 0, true); + return data; +} diff --git a/test/wallet-chainstate-test.js b/test/wallet-chainstate-test.js new file mode 100644 index 000000000..292d2c46e --- /dev/null +++ b/test/wallet-chainstate-test.js @@ -0,0 +1,567 @@ +'use strict'; + +const assert = require('bsert'); +const consensus = require('../lib/protocol/consensus'); +const Network = require('../lib/protocol/network'); +const MTX = require('../lib/primitives/mtx'); +const WorkerPool = require('../lib/workers/workerpool'); +const WalletDB = require('../lib/wallet/walletdb'); +const wutils = require('./util/wallet'); +const { + dummyInput, + nextEntry +} = wutils; + +const enabled = true; +const size = 2; +const network = Network.get('main'); + +describe('WalletDB ChainState', function() { + /** @type {WorkerPool} */ + let workers = null; + /** @type {WalletDB} */ + let wdb = null; + + const progressWithTX = async (wdb) => { + const addr = await wdb.primary.receiveAddress(); + const mtx = new MTX(); + mtx.addInput(dummyInput()); + mtx.addOutput(addr, 10000); + + const block = nextEntry(wdb); + const txs = [mtx.toTX()]; + await wdb.addBlock(block, txs); + + return {block, txs}; + }; + + const progressWithNoTX = async (wdb) => { + const block = nextEntry(wdb); + const txs = []; + + await wdb.addBlock(block, txs); + return {block, txs}; + }; + + beforeEach(async () => { + workers = new WorkerPool({ enabled, size }); + wdb = new WalletDB({ workers, network }); + await workers.open(); + await wdb.open(); + }); + + afterEach(async () => { + await wdb.close(); + await workers.close(); + }); + + it('should have initial state', () => { + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, 0); + assert.strictEqual(wdb.state.height, 0); + assert.strictEqual(wdb.state.marked, false); + }); + + it('should progress height but not startHeight w/o txs', async () => { + const blocks = 10; + + for (let i = 0; i < blocks; i++) { + await progressWithNoTX(wdb); + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, i + 1); + assert.strictEqual(wdb.state.height, i + 1); + assert.strictEqual(wdb.state.marked, false); + } + + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, blocks); + assert.strictEqual(wdb.state.height, blocks); + assert.strictEqual(wdb.state.marked, false); + }); + + it('should change startHeight when receiveing txs', async () => { + const beforeBlocks = 10; + const blocks = 10; + + for (let i = 0; i < beforeBlocks; i++) { + await progressWithNoTX(wdb); + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, i + 1); + assert.strictEqual(wdb.state.height, i + 1); + assert.strictEqual(wdb.state.marked, false); + } + + let firstBlock = null; + for (let i = 0; i < blocks; i++) { + const {block} = await progressWithTX(wdb); + + if (!firstBlock) + firstBlock = block; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, beforeBlocks + i + 1); + assert.strictEqual(wdb.state.height, beforeBlocks + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, beforeBlocks + blocks); + assert.strictEqual(wdb.state.height, beforeBlocks + blocks); + assert.strictEqual(wdb.state.marked, true); + }); + + it('should not change startHeight once marked w/o txs', async () => { + const noTXBlocks1 = 5; + const txBlocks1 = 5; + const noTXBlocks2 = 5; + const txBlocks2 = 5; + + let height = 0; + let firstBlock = null; + + for (let i = 0; i < noTXBlocks1; i++) { + await progressWithNoTX(wdb); + height++; + + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, height); + assert.strictEqual(wdb.state.height, height); + assert.strictEqual(wdb.state.marked, false); + } + + for (let i = 0; i < txBlocks1; i++) { + const {block} = await progressWithTX(wdb); + height++; + + if (!firstBlock) + firstBlock = block; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, height); + assert.strictEqual(wdb.state.height, height); + assert.strictEqual(wdb.state.marked, true); + } + + for (let i = 0; i < noTXBlocks2; i++) { + await progressWithNoTX(wdb); + height++; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, height); + assert.strictEqual(wdb.state.height, height); + assert.strictEqual(wdb.state.marked, true); + } + + for (let i = 0; i < txBlocks2; i++) { + await progressWithTX(wdb); + height++; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, height); + assert.strictEqual(wdb.state.height, height); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBlocks1 + noTXBlocks1 + txBlocks1 + txBlocks2); + assert.strictEqual(wdb.state.height, noTXBlocks1 + noTXBlocks1 + txBlocks1 + txBlocks2); + assert.strictEqual(wdb.state.marked, true); + }); + + it('should not change startHeight once marked on reorg (future reorgs)', async () => { + const noTXBuffer = 10; + const blocksPerAction = 5; + let firstBlock = null; + + for (let i = 0; i < noTXBuffer; i++) + await progressWithNoTX(wdb); + + for (let i = 0; i < blocksPerAction; i++) { + const {block} = await progressWithTX(wdb); + + if (!firstBlock) + firstBlock = block; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.marked, true); + + const removeBlocks = []; + // first 5 blocks with no txs. before reorg. + for (let i = 0; i < blocksPerAction; i++) { + const {block} = await progressWithNoTX(wdb); + removeBlocks.push(block); + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + // Disconnect all the stuff. + for (let i = 0; i < blocksPerAction; i++) { + await wdb.removeBlock(removeBlocks.pop()); + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2 - i - 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2 - i - 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(removeBlocks.length, 0); + + // Reconnect with txs. + for (let i = 0; i < blocksPerAction; i++) { + const {block} = await progressWithTX(wdb); + removeBlocks.push(block); + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + // Disconnect all the stuff again. + for (let i = 0; i < blocksPerAction; i++) { + await wdb.removeBlock(removeBlocks.pop()); + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2 - i - 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2 - i - 1); + assert.strictEqual(wdb.state.marked, true); + } + }); + + it('should should not change start height if reorg recovers txs at same height', async () => { + const noTXBuffer = 10; + const blocksPerAction = 5; + let firstBlock = null; + const removeBlocks = []; + + for (let i = 0; i < noTXBuffer; i++) + await progressWithNoTX(wdb); + + for (let i = 0; i < blocksPerAction; i++) { + const blockAndTxs = await progressWithNoTX(wdb); + removeBlocks.push(blockAndTxs); + } + + assert.strictEqual(wdb.state.startHeight, 0); + assert.bufferEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.marked, false); + + for (let i = 0; i < blocksPerAction; i++) { + const blockAndTxs = await progressWithTX(wdb); + removeBlocks.push(blockAndTxs); + + if (!firstBlock) + firstBlock = blockAndTxs.block; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.marked, true); + + const connectList = removeBlocks.slice(); + + for (let i = 0; i < blocksPerAction - 1; i++) { + const {block} = removeBlocks.pop(); + await wdb.removeBlock(block); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.bufferEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + 1); + assert.strictEqual(wdb.state.marked, true); + + // Remove last block after which chain state becomes unmarked. + { + const {block} = removeBlocks.pop(); + await wdb.removeBlock(block); + const tip = await wdb.getTip(); + + // this block is no longer ours, so it gets unmarked + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.bufferEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.marked, false); + } + + for (let i = 0; i < blocksPerAction; i++) { + const {block} = removeBlocks.pop(); + await wdb.removeBlock(block); + const tip = await wdb.getTip(); + + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.bufferEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction - i - 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction - i - 1); + assert.strictEqual(wdb.state.marked, false); + } + + const tip = await wdb.getTip(); + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.bufferEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer); + assert.strictEqual(wdb.state.height, noTXBuffer); + assert.strictEqual(wdb.state.marked, false); + + // Re add all the blocks. + let marked = false; + firstBlock = null; + + // Marked check only runs when there are transactions, + // so startHeight and startHash will be left behind until + // we find first block with txs. + const checkEntry = { + hash: tip.hash, + height: tip.height + }; + + for (const [i, {block, txs}] of connectList.entries()) { + await wdb.addBlock(block, txs); + + if (!firstBlock && txs.length > 0) { + firstBlock = block; + marked = true; + } + + // First block marks and changes startHash, startHeight + if (firstBlock) { + checkEntry.hash = firstBlock.hash; + checkEntry.height = firstBlock.height; + } + + assert.strictEqual(wdb.state.startHeight, checkEntry.height); + assert.bufferEqual(wdb.state.startHash, checkEntry.hash); + assert.strictEqual(wdb.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.marked, marked); + } + }); + + it('should change mark and startHeight on reorg to earlier', async () => { + const noTXBuffer = 10; + const blocksPerAction = 5; + let firstBlock = null; + const removeBlocks = []; + + for (let i = 0; i < noTXBuffer; i++) + await progressWithNoTX(wdb); + + for (let i = 0; i < blocksPerAction; i++) + removeBlocks.push(await progressWithNoTX(wdb)); + + for (let i = 0; i < blocksPerAction; i++) { + const blockAndTxs = await progressWithTX(wdb); + if (!firstBlock) + firstBlock = blockAndTxs.block; + removeBlocks.push(blockAndTxs); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.marked, true); + + // revert all + for (const {block} of removeBlocks.reverse()) + await wdb.removeBlock(block); + + const tip = await wdb.getTip(); + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.strictEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer); + assert.strictEqual(wdb.state.height, noTXBuffer); + assert.strictEqual(wdb.state.marked, false); + + // create new chain but all with txs. + firstBlock = null; + + for (let i = 0; i < blocksPerAction; i++) { + const blockAndTxs = await progressWithTX(wdb); + + if (!firstBlock) + firstBlock = blockAndTxs.block; + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.marked, true); + }); + + it('should change mark and startHeight on reorg but later', async () => { + const noTXBuffer = 10; + const blocksPerAction = 5; + let firstBlock = null; + const removeBlocks = []; + + for (let i = 0; i < noTXBuffer; i++) + await progressWithNoTX(wdb); + + for (let i = 0; i < blocksPerAction * 2; i++) { + const blockAndTxs = await progressWithTX(wdb); + if (!firstBlock) + firstBlock = blockAndTxs.block; + removeBlocks.push(blockAndTxs); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.marked, true); + + // revert all + for (const {block} of removeBlocks.reverse()) + await wdb.removeBlock(block); + + const tip = await wdb.getTip(); + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.strictEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer); + assert.strictEqual(wdb.state.height, noTXBuffer); + assert.strictEqual(wdb.state.marked, false); + + for (let i = 0; i < blocksPerAction; i++) { + await progressWithNoTX(wdb); + + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.strictEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.marked, false); + } + + assert.strictEqual(wdb.state.startHeight, tip.height); + assert.strictEqual(wdb.state.startHash, tip.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction); + assert.strictEqual(wdb.state.marked, false); + + firstBlock = null; + + for (let i = 0; i < blocksPerAction; i++) { + const blockAndTxs = await progressWithTX(wdb); + if (!firstBlock) + firstBlock = blockAndTxs.block; + removeBlocks.push(blockAndTxs); + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + i + 1); + assert.strictEqual(wdb.state.marked, true); + } + + assert.strictEqual(wdb.state.startHeight, firstBlock.height); + assert.strictEqual(wdb.state.startHash, firstBlock.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction * 2); + assert.strictEqual(wdb.state.marked, true); + }); + + it.skip('should recover to the proper mark/startHeight after corruption', async () => { + // If we receive a block that has TXs (meaning wdb should care) but it + // DB/Node closes/crashes and restarted node does not have txs in the blocks. + // startHeight and mark will be set incorrectly. + const noTXBuffer = 10; + const blocksPerAction = 5; + + for (let i = 0; i < noTXBuffer; i++) + await progressWithNoTX(wdb); + + assert.strictEqual(wdb.state.startHeight, 0); + assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, noTXBuffer); + assert.strictEqual(wdb.state.height, noTXBuffer); + assert.strictEqual(wdb.state.marked, false); + + // This will be the corruption case. + const bakAdd = wdb.primary.add; + wdb.primary.add = () => { + throw new Error('Corruption'); + }; + + let err; + try { + await progressWithTX(wdb); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'Corruption'); + + assert.strictEqual(wdb.state.startHeight, 0); + assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, noTXBuffer); + assert.strictEqual(wdb.state.height, noTXBuffer); + assert.strictEqual(wdb.state.marked, false); + + wdb.primary.add = bakAdd; + + // no tx blocks... + for (let i = 0; i < blocksPerAction; i++) { + await progressWithNoTX(wdb); + + assert.strictEqual(wdb.state.startHeight, 0); + assert.strictEqual(wdb.state.startHash, consensus.ZERO_HASH); + assert.strictEqual(wdb.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + i + 1); + assert.strictEqual(wdb.state.marked, false); + } + + const {block} = await progressWithTX(wdb); + assert.strictEqual(wdb.state.startHeight, block.height); + assert.strictEqual(wdb.state.startHash, block.hash); + assert.strictEqual(wdb.height, noTXBuffer + blocksPerAction + 1); + assert.strictEqual(wdb.state.height, noTXBuffer + blocksPerAction + 1); + assert.strictEqual(wdb.state.marked, true); + }); +}); diff --git a/test/wallet-test.js b/test/wallet-test.js index 6dc2b00b6..9119c2124 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -5,16 +5,13 @@ const WalletClient = require('../lib/client/wallet'); 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'); const MTX = require('../lib/primitives/mtx'); -const ChainEntry = require('../lib/blockchain/chainentry'); const {Resource} = require('../lib/dns/resource'); -const Block = require('../lib/primitives/block'); const Coin = require('../lib/primitives/coin'); const KeyRing = require('../lib/primitives/keyring'); const Input = require('../lib/primitives/input'); @@ -27,6 +24,14 @@ const Mnemonic = require('../lib/hd/mnemonic'); const Wallet = require('../lib/wallet/wallet'); const rules = require('../lib/covenants/rules'); const {forValue} = require('./util/common'); +const wutils = require('./util/wallet'); +const { + dummyInput, + curBlock, + nextBlock, + curEntry, + nextEntry +} = wutils; const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -47,57 +52,6 @@ let doubleSpendWallet = null; let doubleSpendCoin = null; let watchWallet = null; -function fromU32(num) { - const data = Buffer.allocUnsafe(4); - data.writeUInt32LE(num, 0, true); - return data; -} - -function curBlock(wdb) { - return fakeBlock(wdb.state.height); -}; - -function nextBlock(wdb) { - return fakeBlock(wdb.state.height + 1); -} - -function fakeBlock(height) { - const prev = blake2b.digest(fromU32((height - 1) >>> 0)); - const hash = blake2b.digest(fromU32(height >>> 0)); - const root = blake2b.digest(fromU32((height | 0x80000000) >>> 0)); - - return { - hash: hash, - prevBlock: prev, - merkleRoot: root, - time: 500000000 + (height * (10 * 60)), - bits: 0, - nonce: 0, - height: height, - version: 0, - witnessRoot: Buffer.alloc(32), - treeRoot: Buffer.alloc(32), - reservedRoot: Buffer.alloc(32), - extraNonce: Buffer.alloc(24), - mask: Buffer.alloc(32) - }; -} - -function curEntry(wdb) { - return new ChainEntry(curBlock(wdb)); -} - -function nextEntry(wdb) { - const cur = curEntry(wdb); - const next = new Block(nextBlock(wdb)); - return ChainEntry.fromBlock(next, cur); -} - -function dummyInput() { - const hash = random.randomBytes(32); - return Input.fromOutpoint(new Outpoint(hash, 0)); -} - describe('Wallet', function() { this.timeout(5000); From 2601f3cee80fe87c30d97bfc8e419848bdc4c733 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Fri, 3 Nov 2023 13:19:13 +0400 Subject: [PATCH 2/2] walletdb: make markState part of the setTip. Fixes two markState issues: - On close/crash of walletdb on add block wdb state would have incorrect state for the startHeight/startHash and mark. WalletDB could end up having incorrect startHeight/startHash for the rest of the walletDB until rescan before height. Check the wallet-chainstate-test case for corruption. - On the specific height where the walletDB received first transaction, state.height would also be incorrect (only for one block). --- lib/wallet/walletdb.js | 48 ++++++++++++---------------------- test/wallet-chainstate-test.js | 2 +- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 2c27d5711..ae842c6ab 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1839,13 +1839,24 @@ class WalletDB extends EventEmitter { /** * Sync the current chain state to tip. * @param {BlockMeta} tip + * @param {Boolean} checkMark - should we check startHeight/mark. This should + * only happen if we are progressing forward in history and have txs. * @returns {Promise} */ - async setTip(tip) { + async setTip(tip, checkMark = false) { const b = this.db.batch(); const state = this.state.clone(); + // mark state if state has not been marked, we are moving forward + // and we have txs. If state is marked, it means we already found + // first tx for the whole wdb, so no longer move it forward. + if (checkMark && !state.marked) { + state.startHeight = tip.height; + state.startHash = tip.hash; + state.marked = true; + } + if (tip.height < state.height) { // Hashes ahead of our new tip // that we need to delete. @@ -1890,26 +1901,6 @@ class WalletDB extends EventEmitter { return height; } - /** - * Mark current state. - * @param {BlockMeta} block - * @returns {Promise} - */ - - async markState(block) { - const state = this.state.clone(); - state.startHeight = block.height; - state.startHash = block.hash; - state.marked = true; - - const b = this.db.batch(); - b.put(layout.R.encode(), state.encode()); - await b.write(); - - this.state = state; - this.height = state.height; - } - /** * Get a wallet map. * @param {Buffer} key @@ -2209,7 +2200,7 @@ class WalletDB extends EventEmitter { assert(tip); await this.revert(tip.height); - await this.setTip(tip); + await this.setTip(tip, false); } /** @@ -2309,7 +2300,8 @@ class WalletDB extends EventEmitter { } // Sync the state to the new tip. - await this.setTip(tip); + // If we encountered wallet txs, we also trigger mark check. + await this.setTip(tip, walletTxs.length > 0); } finally { this.confirming = false; } @@ -2370,7 +2362,7 @@ class WalletDB extends EventEmitter { const map = await this.getBlockMap(tip.height); if (!map) { - await this.setTip(prev); + await this.setTip(prev, false); this.emit('block disconnect', entry); return 0; } @@ -2384,7 +2376,7 @@ class WalletDB extends EventEmitter { } // Sync the state to the previous tip. - await this.setTip(prev); + await this.setTip(prev, false); this.logger.warning('Disconnected wallet block %x (tx=%d).', tip.hash, total); @@ -2454,12 +2446,6 @@ class WalletDB extends EventEmitter { if (!wids) return null; - // This is better place than _addBlock because here above check - // makes sure we really own the txs and it was not a false positive - // from the filter. - if (block && !this.state.marked) - await this.markState(block); - this.logger.info( 'Incoming transaction for %d wallets in WalletDB (%s).', wids.size, tx.txid()); diff --git a/test/wallet-chainstate-test.js b/test/wallet-chainstate-test.js index 292d2c46e..0cb32a2c7 100644 --- a/test/wallet-chainstate-test.js +++ b/test/wallet-chainstate-test.js @@ -506,7 +506,7 @@ describe('WalletDB ChainState', function() { assert.strictEqual(wdb.state.marked, true); }); - it.skip('should recover to the proper mark/startHeight after corruption', async () => { + it('should recover to the proper mark/startHeight after corruption', async () => { // If we receive a block that has TXs (meaning wdb should care) but it // DB/Node closes/crashes and restarted node does not have txs in the blocks. // startHeight and mark will be set incorrectly.