|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2023 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | + |
| 6 | +""" |
| 7 | +Test that wallet correctly tracks transactions that have been conflicted by blocks, particularly during reorgs. |
| 8 | +""" |
| 9 | + |
| 10 | +from decimal import Decimal |
| 11 | + |
| 12 | +from test_framework.test_framework import BitcoinTestFramework |
| 13 | +from test_framework.util import ( |
| 14 | + assert_equal, |
| 15 | +) |
| 16 | + |
| 17 | +class TxConflicts(BitcoinTestFramework): |
| 18 | + def add_options(self, parser): |
| 19 | + self.add_wallet_options(parser) |
| 20 | + |
| 21 | + def set_test_params(self): |
| 22 | + self.num_nodes = 3 |
| 23 | + |
| 24 | + def skip_test_if_missing_module(self): |
| 25 | + self.skip_if_no_wallet() |
| 26 | + |
| 27 | + def get_utxo_of_value(self, from_tx_id, search_value): |
| 28 | + return next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(from_tx_id)["details"] if tx_out["amount"] == Decimal(f"{search_value}")) |
| 29 | + |
| 30 | + def run_test(self): |
| 31 | + self.log.info("Send tx from which to conflict outputs later") |
| 32 | + txid_conflict_from_1 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) |
| 33 | + txid_conflict_from_2 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) |
| 34 | + self.generate(self.nodes[0], 1) |
| 35 | + self.sync_blocks() |
| 36 | + |
| 37 | + self.log.info("Disconnect nodes to broadcast conflicts on their respective chains") |
| 38 | + self.disconnect_nodes(0, 1) |
| 39 | + self.disconnect_nodes(2, 1) |
| 40 | + |
| 41 | + self.log.info("Create transactions that conflict with each other") |
| 42 | + output_A = self.get_utxo_of_value(from_tx_id=txid_conflict_from_1, search_value=10) |
| 43 | + output_B = self.get_utxo_of_value(from_tx_id=txid_conflict_from_2, search_value=10) |
| 44 | + |
| 45 | + # First create a transaction that consumes both A and B outputs. |
| 46 | + # |
| 47 | + # | tx1 | -----> | | | | |
| 48 | + # | AB_parent_tx | ----> | Child_Tx | |
| 49 | + # | tx2 | -----> | | | | |
| 50 | + # |
| 51 | + inputs_tx_AB_parent = [{"txid": txid_conflict_from_1, "vout": output_A}, {"txid": txid_conflict_from_2, "vout": output_B}] |
| 52 | + tx_AB_parent = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_AB_parent, {self.nodes[0].getnewaddress(): Decimal("19.99998")})) |
| 53 | + |
| 54 | + # Secondly, create two transactions: One consuming output_A, and another one consuming output_B |
| 55 | + # |
| 56 | + # | tx1 | -----> | Tx_A_1 | |
| 57 | + # ---------------- |
| 58 | + # | tx2 | -----> | Tx_B_1 | |
| 59 | + # |
| 60 | + inputs_tx_A_1 = [{"txid": txid_conflict_from_1, "vout": output_A}] |
| 61 | + inputs_tx_B_1 = [{"txid": txid_conflict_from_2, "vout": output_B}] |
| 62 | + tx_A_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_A_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")})) |
| 63 | + tx_B_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_B_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")})) |
| 64 | + |
| 65 | + self.log.info("Broadcast conflicted transaction") |
| 66 | + txid_AB_parent = self.nodes[0].sendrawtransaction(tx_AB_parent["hex"]) |
| 67 | + self.generate(self.nodes[0], 1, sync_fun=self.no_op) |
| 68 | + |
| 69 | + # Now that 'AB_parent_tx' was broadcast, build 'Child_Tx' |
| 70 | + output_c = self.get_utxo_of_value(from_tx_id=txid_AB_parent, search_value=19.99998) |
| 71 | + inputs_tx_C_child = [({"txid": txid_AB_parent, "vout": output_c})] |
| 72 | + |
| 73 | + tx_C_child = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_C_child, {self.nodes[0].getnewaddress() : Decimal("19.99996")})) |
| 74 | + tx_C_child_txid = self.nodes[0].sendrawtransaction(tx_C_child["hex"]) |
| 75 | + self.generate(self.nodes[0], 1, sync_fun=self.no_op) |
| 76 | + |
| 77 | + self.log.info("Broadcast conflicting tx to node 1 and generate a longer chain") |
| 78 | + conflicting_txid_A = self.nodes[1].sendrawtransaction(tx_A_1["hex"]) |
| 79 | + self.generate(self.nodes[1], 4, sync_fun=self.no_op) |
| 80 | + conflicting_txid_B = self.nodes[1].sendrawtransaction(tx_B_1["hex"]) |
| 81 | + self.generate(self.nodes[1], 4, sync_fun=self.no_op) |
| 82 | + |
| 83 | + self.log.info("Connect nodes 0 and 1, trigger reorg and ensure that the tx is effectively conflicted") |
| 84 | + self.connect_nodes(0, 1) |
| 85 | + self.sync_blocks([self.nodes[0], self.nodes[1]]) |
| 86 | + conflicted_AB_tx = self.nodes[0].gettransaction(txid_AB_parent) |
| 87 | + tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid) |
| 88 | + conflicted_A_tx = self.nodes[0].gettransaction(conflicting_txid_A) |
| 89 | + |
| 90 | + self.log.info("Verify, after the reorg, that Tx_A was accepted, and tx_AB and its Child_Tx are conflicting now") |
| 91 | + # Tx A was accepted, Tx AB was not. |
| 92 | + assert conflicted_AB_tx["confirmations"] < 0 |
| 93 | + assert conflicted_A_tx["confirmations"] > 0 |
| 94 | + |
| 95 | + # Conflicted tx should have confirmations set to the confirmations of the most conflicting tx |
| 96 | + assert_equal(-conflicted_AB_tx["confirmations"], conflicted_A_tx["confirmations"]) |
| 97 | + # Child should inherit conflicted state from parent |
| 98 | + assert_equal(-tx_C_child["confirmations"], conflicted_A_tx["confirmations"]) |
| 99 | + # Check the confirmations of the conflicting transactions |
| 100 | + assert_equal(conflicted_A_tx["confirmations"], 8) |
| 101 | + assert_equal(self.nodes[0].gettransaction(conflicting_txid_B)["confirmations"], 4) |
| 102 | + |
| 103 | + self.log.info("Now generate a longer chain that does not contain any tx") |
| 104 | + # Node2 chain without conflicts |
| 105 | + self.generate(self.nodes[2], 15, sync_fun=self.no_op) |
| 106 | + |
| 107 | + # Connect node0 and node2 and wait reorg |
| 108 | + self.connect_nodes(0, 2) |
| 109 | + self.sync_blocks() |
| 110 | + conflicted = self.nodes[0].gettransaction(txid_AB_parent) |
| 111 | + tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid) |
| 112 | + |
| 113 | + self.log.info("Test that formerly conflicted transaction are inactive after reorg") |
| 114 | + # Former conflicted tx should be unconfirmed as it hasn't been yet rebroadcast |
| 115 | + assert_equal(conflicted["confirmations"], 0) |
| 116 | + # Former conflicted child tx should be unconfirmed as it hasn't been rebroadcast |
| 117 | + assert_equal(tx_C_child["confirmations"], 0) |
| 118 | + # Rebroadcast former conflicted tx and check it confirms smoothly |
| 119 | + self.nodes[2].sendrawtransaction(conflicted["hex"]) |
| 120 | + self.generate(self.nodes[2], 1) |
| 121 | + self.sync_blocks() |
| 122 | + former_conflicted = self.nodes[0].gettransaction(txid_AB_parent) |
| 123 | + assert_equal(former_conflicted["confirmations"], 1) |
| 124 | + assert_equal(former_conflicted["blockheight"], 217) |
| 125 | + |
| 126 | +if __name__ == '__main__': |
| 127 | + TxConflicts().main() |
0 commit comments