/ test / functional / wallet_abandonconflict.py
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()