Repository: bitcoin/bitcoin
File: test/functional/wallet_reorgsrestore.py (Python)

Code before:
        # Disconnect node1 from others to reorg its chain later
        self.disconnect_nodes(0, 1)
        self.disconnect_nodes(1, 2)
        self.connect_nodes(0, 2)

        # Send a tx to be unconfirmed later
        txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
        tx = self.nodes[0].gettransaction(txid)
        self.generate(self.nodes[0], 4, sync_fun=self.no_op)
        self.sync_blocks([self.nodes[0], self.nodes[2]])
        tx_before_reorg = self.nodes[0].gettransaction(txid)
        assert_equal(tx_before_reorg["confirmations"], 4)

        # Disconnect node0 from node2 to broadcast a conflict on their respective chains
        self.disconnect_nodes(0, 2)
        nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10"))
        inputs = []
        inputs.append({"txid": txid_conflict_from, "vout": nA})
        outputs_1 = {}
        outputs_2 = {}

        # Create a conflicted tx broadcast on node0 chain and conflicting tx broadcast on node1 chain. Both spend from txid_conflict_from
        outputs_1[self.nodes[0].getnewaddress()] = Decimal("9.99998")
        outputs_2[self.nodes[0].getnewaddress()] = Decimal("9.99998")
        conflicted = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_1))
        conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2))

        conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"])
        self.generate(self.nodes[0], 1, sync_fun=self.no_op)
        conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"])
        self.generate(self.nodes[2], 9, sync_fun=self.no_op)

        # Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted
        self.connect_nodes(0, 2)
        self.sync_blocks([self.nodes[0], self.nodes[2]])
        conflicted = self.nodes[0].gettransaction(conflicted_txid)
        conflicting = self.nodes[0].gettransaction(conflicting_txid)
        assert_equal(conflicted["confirmations"], -9)
        assert_equal(conflicted["walletconflicts"][0], conflicting["txid"])

        # Node0 wallet is shutdown
        self.restart_node(0)

        # The block chain re-orgs and the tx is included in a different block
        self.generate(self.nodes[1], 9, sync_fun=self.no_op)
        self.nodes[1].sendrawtransaction(tx["hex"])
        self.generate(self.nodes[1], 1, sync_fun=self.no_op)
        self.nodes[1].sendrawtransaction(conflicted["hex"])
        self.generate(self.nodes[1], 1, sync_fun=self.no_op)

        # Node0 wallet file is loaded on longest sync'ed node1

Review comment (bug):
Re: https://github.com/bitcoin/bitcoin/pull/31757#discussion_r1961637704
Continuing this thread here because there doesn't seem to be a way to continue that thread while making it part of the review.

As per this comment and related code https://github.com/bitcoin/bitcoin/issues/31824#issuecomment-2669011985, I understand that `FlushStateMode::IF_NEEDED` mode is used and therefore I assume that makes it quite unlikely to flush chainstate in a functional test, so the test will not fail.

> The test will still pass if the best locator is flushed to disk before the abrupt shutdown. And that’s fine, that’s the expected behavior.

However, I don't understand this comment. Hypothetically, if the state were to be flushed before abrupt node kill, wouldn't this assertion here fail?

Code after:
        wallet = node.get_wallet_rpc("reorg_crash")
        self.generatetoaddress(node, 1, wallet.getnewaddress(), sync_fun=self.no_op)

        # Restart to ensure node and wallet are flushed
        self.restart_node(0)
        wallet = node.get_wallet_rpc("reorg_crash")
        assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0)

        # Disconnect tip and sync wallet state
        tip = wallet.getbestblockhash()
        wallet.invalidateblock(tip)
        wallet.syncwithvalidationinterfacequeue()

        # Tip was disconnected, ensure coinbase has been abandoned
        assert_equal(wallet.getwalletinfo()['immature_balance'], 0)
        coinbase_tx_id = wallet.getblock(tip, verbose=1)["tx"][0]
        assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True)

        # Abort process abruptly to mimic an unclean shutdown (no chain state flush to disk)
        node.process.kill()

        # Restart the node and confirm that it has not persisted the last chain state changes to disk
        self.start_node(0)
        assert_equal(node.getbestblockhash(), tip)

        # Due to an existing bug, the wallet incorrectly keeps the transaction in an abandoned state, even though that's
        # no longer the case (after the unclean shutdown, the node's chain returned to the pre-invalidation tip).
        # This issue blocks any future spending and results in an incorrect balance display.
        wallet = node.get_wallet_rpc("reorg_crash")
        assert_equal(wallet.getwalletinfo()['immature_balance'], 0) # FIXME: #31824.

        # Previously, a bug caused the node to crash if two block disconnection events occurred consecutively.
        # Ensure this is no longer the case by simulating a new reorg.
        node.invalidateblock(tip)
        assert(node.getbestblockhash() != tip)
        # Ensure wallet state is consistent now
        assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True)
        assert_equal(wallet.getwalletinfo()['immature_balance'], 0)

        # And finally, verify the state if the block ends up being into the best chain again
        node.reconsiderblock(tip)
        assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False)
        assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0)

    def run_test(self):
        # Send a tx from which to conflict outputs later
        txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
        self.generate(self.nodes[0], 1)

        # Disconnect node1 from others to reorg its chain later
        self.disconnect_nodes(0, 1)