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

Code before:
        # Verify the coinbase tx was marked as abandoned and balance correctly computed
        tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0]
        assert_equal(tx_info['abandoned'], True)
        assert_equal(tx_info['category'], 'orphan')
        assert(wallet0.getbalances()['mine']['trusted'] == 0)
        # Verify the coinbase descendant was also marked as abandoned
        assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)


    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)
        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)

Review comment (suggestion):
Can add a comment here highlighting that this RPC call is added to ensure the block disconnected notification is handled by the wallet.

Code after:

        # Verify the coinbase tx was marked as abandoned and balance correctly computed
        tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0]
        assert_equal(tx_info['abandoned'], True)
        assert_equal(tx_info['category'], 'orphan')
        assert(wallet0.getbalances()['mine']['trusted'] == 0)
        # Verify the coinbase descendant was also marked as abandoned
        assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)

    def test_reorg_handling_during_unclean_shutdown(self):
        self.log.info("Test that wallet doesn't crash due to a duplicate block disconnection event after an unclean shutdown")
        node = self.nodes[0]
        # Receive coinbase reward on a new wallet
        node.createwallet(wallet_name="reorg_crash", load_on_startup=True)
        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)