wallet_abandonconflict.py raw

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