wallet_abandonconflict.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2014-2022 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 """Test the abandontransaction RPC. 6 7 The abandontransaction RPC marks a transaction and all its in-wallet 8 descendants as abandoned which allows their inputs to be respent. It can be 9 used to replace "stuck" or evicted transactions. It only works on transactions 10 which are not included in a block and are not currently in the mempool. It has 11 no effect on transactions which are already abandoned. 12 """ 13 from decimal import Decimal 14 15 from test_framework.blocktools import COINBASE_MATURITY 16 from test_framework.test_framework import BitcoinTestFramework 17 from test_framework.util import ( 18 assert_equal, 19 assert_raises_rpc_error, 20 ) 21 22 23 class AbandonConflictTest(BitcoinTestFramework): 24 def add_options(self, parser): 25 self.add_wallet_options(parser) 26 27 def set_test_params(self): 28 self.num_nodes = 2 29 self.extra_args = [["-minrelaytxfee=0.00001"], []] 30 # whitelist peers to speed up tx relay / mempool sync 31 self.noban_tx_relay = True 32 33 def skip_test_if_missing_module(self): 34 self.skip_if_no_wallet() 35 36 def run_test(self): 37 # create two wallets to tests conflicts from both sender's and receiver's sides 38 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 39 self.nodes[0].createwallet(wallet_name="bob") 40 bob = self.nodes[0].get_wallet_rpc("bob") 41 42 self.generate(self.nodes[1], COINBASE_MATURITY) 43 balance = alice.getbalance() 44 txA = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) 45 txB = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) 46 txC = alice.sendtoaddress(alice.getnewaddress(), Decimal("10")) 47 self.sync_mempools() 48 self.generate(self.nodes[1], 1) 49 50 # Can not abandon non-wallet transaction 51 assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: alice.abandontransaction(txid='ff' * 32)) 52 # Can not abandon confirmed transaction 53 assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA)) 54 55 newbalance = alice.getbalance() 56 assert balance - newbalance < Decimal("0.001") #no more than fees lost 57 balance = newbalance 58 59 # Disconnect nodes so node0's transactions don't get into node1's mempool 60 self.disconnect_nodes(0, 1) 61 62 # Identify the 10btc outputs 63 nA = next(tx_out["vout"] for tx_out in alice.gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10")) 64 nB = next(tx_out["vout"] for tx_out in alice.gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10")) 65 nC = next(tx_out["vout"] for tx_out in alice.gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10")) 66 67 inputs = [] 68 # spend 10btc outputs from txA and txB 69 inputs.append({"txid": txA, "vout": nA}) 70 inputs.append({"txid": txB, "vout": nB}) 71 outputs = {} 72 73 outputs[alice.getnewaddress()] = Decimal("14.99998") 74 outputs[bob.getnewaddress()] = Decimal("5") 75 signed = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) 76 txAB1 = self.nodes[0].sendrawtransaction(signed["hex"]) 77 78 # Identify the 14.99998btc output 79 nAB = next(tx_out["vout"] for tx_out in alice.gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998")) 80 81 #Create a child tx spending AB1 and C 82 inputs = [] 83 inputs.append({"txid": txAB1, "vout": nAB}) 84 inputs.append({"txid": txC, "vout": nC}) 85 outputs = {} 86 outputs[alice.getnewaddress()] = Decimal("24.9996") 87 signed2 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) 88 txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"]) 89 90 # Create a child tx spending ABC2 91 signed3_change = Decimal("24.999") 92 inputs = [{"txid": txABC2, "vout": 0}] 93 outputs = {alice.getnewaddress(): signed3_change} 94 signed3 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs)) 95 # note tx is never directly referenced, only abandoned as a child of the above 96 self.nodes[0].sendrawtransaction(signed3["hex"]) 97 98 # In mempool txs from self should increase balance from change 99 newbalance = alice.getbalance() 100 assert_equal(newbalance, balance - Decimal("30") + signed3_change) 101 balance = newbalance 102 103 # Restart the node with a higher min relay fee so the parent tx is no longer in mempool 104 # TODO: redo with eviction 105 self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"]) 106 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 107 assert self.nodes[0].getmempoolinfo()['loaded'] 108 109 # Verify txs no longer in either node's mempool 110 assert_equal(len(self.nodes[0].getrawmempool()), 0) 111 assert_equal(len(self.nodes[1].getrawmempool()), 0) 112 113 # Not in mempool txs from self should only reduce balance 114 # inputs are still spent, but change not received 115 newbalance = alice.getbalance() 116 assert_equal(newbalance, balance - signed3_change) 117 # Unconfirmed received funds that are not in mempool, also shouldn't show 118 # up in unconfirmed balance 119 balances = alice.getbalances()['mine'] 120 assert_equal(balances['untrusted_pending'] + balances['trusted'], newbalance) 121 # Also shouldn't show up in listunspent 122 assert not txABC2 in [utxo["txid"] for utxo in alice.listunspent(0)] 123 balance = newbalance 124 125 # Abandon original transaction and verify inputs are available again 126 # including that the child tx was also abandoned 127 alice.abandontransaction(txAB1) 128 newbalance = alice.getbalance() 129 assert_equal(newbalance, balance + Decimal("30")) 130 balance = newbalance 131 132 self.log.info("Check abandoned transactions in listsinceblock") 133 listsinceblock = alice.listsinceblock() 134 txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send'] 135 for tx in txAB1_listsinceblock: 136 assert_equal(tx['abandoned'], True) 137 assert_equal(tx['confirmations'], 0) 138 assert_equal(tx['trusted'], False) 139 140 # Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned 141 self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"]) 142 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 143 assert self.nodes[0].getmempoolinfo()['loaded'] 144 145 assert_equal(len(self.nodes[0].getrawmempool()), 0) 146 assert_equal(alice.getbalance(), balance) 147 148 # But if it is received again then it is unabandoned 149 # And since now in mempool, the change is available 150 # But its child tx remains abandoned 151 self.nodes[0].sendrawtransaction(signed["hex"]) 152 newbalance = alice.getbalance() 153 assert_equal(newbalance, balance - Decimal("20") + Decimal("14.99998")) 154 balance = newbalance 155 156 # Send child tx again so it is unabandoned 157 self.nodes[0].sendrawtransaction(signed2["hex"]) 158 newbalance = alice.getbalance() 159 assert_equal(newbalance, balance - Decimal("10") - Decimal("14.99998") + Decimal("24.9996")) 160 balance = newbalance 161 162 # Remove using high relay fee again 163 self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"]) 164 alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 165 assert self.nodes[0].getmempoolinfo()['loaded'] 166 assert_equal(len(self.nodes[0].getrawmempool()), 0) 167 newbalance = alice.getbalance() 168 assert_equal(newbalance, balance - Decimal("24.9996")) 169 balance = newbalance 170 171 self.log.info("Test transactions conflicted by a double spend") 172 self.nodes[0].loadwallet("bob") 173 bob = self.nodes[0].get_wallet_rpc("bob") 174 175 # Create a double spend of AB1 by spending again from only A's 10 output 176 # Mine double spend from node 1 177 inputs = [] 178 inputs.append({"txid": txA, "vout": nA}) 179 outputs = {} 180 outputs[self.nodes[1].getnewaddress()] = Decimal("3.9999") 181 outputs[bob.getnewaddress()] = Decimal("5.9999") 182 tx = alice.createrawtransaction(inputs, outputs) 183 signed = alice.signrawtransactionwithwallet(tx) 184 double_spend_txid = self.nodes[1].sendrawtransaction(signed["hex"]) 185 self.connect_nodes(0, 1) 186 self.generate(self.nodes[1], 1) 187 188 tx_list = alice.listtransactions() 189 190 conflicted = [tx for tx in tx_list if tx["confirmations"] < 0] 191 assert_equal(4, len(conflicted)) 192 193 wallet_conflicts = [tx for tx in conflicted if tx["walletconflicts"]] 194 assert_equal(2, len(wallet_conflicts)) 195 196 double_spends = [tx for tx in tx_list if tx["walletconflicts"] and tx["confirmations"] > 0] 197 assert_equal(2, len(double_spends)) # one for each output 198 double_spend = double_spends[0] 199 200 # Test the properties of the conflicted transactions, i.e. with confirmations < 0. 201 for tx in conflicted: 202 assert_equal(tx["abandoned"], False) 203 assert_equal(tx["confirmations"], -1) 204 assert_equal(tx["trusted"], False) 205 206 # Test the properties of the double-spend transaction, i.e. having wallet conflicts and confirmations > 0. 207 assert_equal(double_spend["abandoned"], False) 208 assert_equal(double_spend["confirmations"], 1) 209 assert "trusted" not in double_spend.keys() # "trusted" only returned if tx has 0 or negative confirmations. 210 211 # Test the walletconflicts field of each. 212 for tx in wallet_conflicts: 213 assert_equal(double_spend["walletconflicts"], [tx["txid"]]) 214 assert_equal(tx["walletconflicts"], [double_spend["txid"]]) 215 216 # Test walletconflicts on the receiver's side 217 txinfo = bob.gettransaction(txAB1) 218 assert_equal(txinfo['confirmations'], -1) 219 assert_equal(txinfo['walletconflicts'], [double_spend['txid']]) 220 221 double_spends = [tx for tx in bob.listtransactions() if tx["walletconflicts"] and tx["confirmations"] > 0] 222 assert_equal(1, len(double_spends)) 223 double_spend = double_spends[0] 224 assert_equal(double_spend_txid, double_spend['txid']) 225 assert_equal(double_spend["walletconflicts"], [txAB1]) 226 227 # Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted 228 assert_equal(alice.gettransaction(txAB1)["confirmations"], -1) 229 newbalance = alice.getbalance() 230 assert_equal(newbalance, balance + Decimal("20")) 231 balance = newbalance 232 233 # Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available 234 blk = self.nodes[0].getbestblockhash() 235 # mine 10 blocks so that when the blk is invalidated, the transactions are not 236 # returned to the mempool 237 self.generate(self.nodes[1], 10) 238 self.nodes[0].invalidateblock(blk) 239 assert_equal(alice.gettransaction(txAB1)["confirmations"], 0) 240 newbalance = alice.getbalance() 241 assert_equal(newbalance, balance - Decimal("20")) 242 243 if __name__ == '__main__': 244 AbandonConflictTest().main()