wallet_abandonconflict.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-present 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 set_test_params(self):
  25          self.num_nodes = 2
  26          self.extra_args = [["-minrelaytxfee=0.00001"], []]
  27          # whitelist peers to speed up tx relay / mempool sync
  28          self.noban_tx_relay = True
  29  
  30      def skip_test_if_missing_module(self):
  31          self.skip_if_no_wallet()
  32  
  33      def run_test(self):
  34          # create two wallets to tests conflicts from both sender's and receiver's sides
  35          alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
  36          self.nodes[0].createwallet(wallet_name="bob")
  37          bob = self.nodes[0].get_wallet_rpc("bob")
  38  
  39          self.generate(self.nodes[1], COINBASE_MATURITY)
  40          balance = alice.getbalance()
  41          txA = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
  42          txB = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
  43          txC = alice.sendtoaddress(alice.getnewaddress(), Decimal("10"))
  44          self.sync_mempools()
  45  
  46          # Can not abandon transaction in mempool
  47          assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))
  48  
  49          self.generate(self.nodes[1], 1)
  50  
  51          # Can not abandon non-wallet transaction
  52          assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', lambda: alice.abandontransaction(txid='ff' * 32))
  53          # Can not abandon confirmed transaction
  54          assert_raises_rpc_error(-5, 'Transaction not eligible for abandonment', lambda: alice.abandontransaction(txid=txA))
  55  
  56          newbalance = alice.getbalance()
  57          assert balance - newbalance < Decimal("0.001")  #no more than fees lost
  58          balance = newbalance
  59  
  60          # Disconnect nodes so node0's transactions don't get into node1's mempool
  61          self.disconnect_nodes(0, 1)
  62  
  63          # Identify the 10btc outputs
  64          nA = next(tx_out["vout"] for tx_out in alice.gettransaction(txA)["details"] if tx_out["amount"] == Decimal("10"))
  65          nB = next(tx_out["vout"] for tx_out in alice.gettransaction(txB)["details"] if tx_out["amount"] == Decimal("10"))
  66          nC = next(tx_out["vout"] for tx_out in alice.gettransaction(txC)["details"] if tx_out["amount"] == Decimal("10"))
  67  
  68          inputs = []
  69          # spend 10btc outputs from txA and txB
  70          inputs.append({"txid": txA, "vout": nA})
  71          inputs.append({"txid": txB, "vout": nB})
  72          outputs = {}
  73  
  74          outputs[alice.getnewaddress()] = Decimal("14.99998")
  75          outputs[bob.getnewaddress()] = Decimal("5")
  76          signed = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
  77          txAB1 = self.nodes[0].sendrawtransaction(signed["hex"])
  78  
  79          # Identify the 14.99998btc output
  80          nAB = next(tx_out["vout"] for tx_out in alice.gettransaction(txAB1)["details"] if tx_out["amount"] == Decimal("14.99998"))
  81  
  82          #Create a child tx spending AB1 and C
  83          inputs = []
  84          inputs.append({"txid": txAB1, "vout": nAB})
  85          inputs.append({"txid": txC, "vout": nC})
  86          outputs = {}
  87          outputs[alice.getnewaddress()] = Decimal("24.9996")
  88          signed2 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
  89          txABC2 = self.nodes[0].sendrawtransaction(signed2["hex"])
  90  
  91          # Create a child tx spending ABC2
  92          signed3_change = Decimal("24.999")
  93          inputs = [{"txid": txABC2, "vout": 0}]
  94          outputs = {alice.getnewaddress(): signed3_change}
  95          signed3 = alice.signrawtransactionwithwallet(alice.createrawtransaction(inputs, outputs))
  96          # note tx is never directly referenced, only abandoned as a child of the above
  97          self.nodes[0].sendrawtransaction(signed3["hex"])
  98  
  99          # In mempool txs from self should increase balance from change
 100          newbalance = alice.getbalance()
 101          assert_equal(newbalance, balance - Decimal("30") + signed3_change)
 102          balance = newbalance
 103  
 104          # Restart the node with a higher min relay fee so the parent tx is no longer in mempool
 105          # TODO: redo with eviction
 106          self.restart_node(0, extra_args=["-minrelaytxfee=0.0001"])
 107          alice = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
 108          assert self.nodes[0].getmempoolinfo()['loaded']
 109  
 110          # Verify txs no longer in either node's mempool
 111          assert_equal(len(self.nodes[0].getrawmempool()), 0)
 112          assert_equal(len(self.nodes[1].getrawmempool()), 0)
 113  
 114          # Not in mempool txs from self should only reduce balance
 115          # inputs are still spent, but change not received
 116          newbalance = alice.getbalance()
 117          assert_equal(newbalance, balance - signed3_change)
 118          # Unconfirmed received funds that are not in mempool
 119          balances = alice.getbalances()['mine']
 120          assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance)
 121          # Also shouldn't show up in listunspent
 122          assert txABC2 not 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(__file__).main()
 245