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):
I would have used node RPCs here because these don't seem to be wallet specific requests and moreover, there's just one loaded wallet and using wallet RPCs here made me think there was some specific reason for it, but fine. I don't want the ACKs to be invalidated for this.

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)