wallet_txn_doublespend.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 wallet accounts properly when there is a double-spend conflict."""
   6  from decimal import Decimal
   7  
   8  from test_framework.test_framework import LimenkaTestFramework
   9  from test_framework.util import (
  10      assert_equal,
  11  )
  12  
  13  
  14  class TxnMallTest(LimenkaTestFramework):
  15      def set_test_params(self):
  16          self.num_nodes = 3
  17          self.supports_cli = False
  18  
  19      def skip_test_if_missing_module(self):
  20          self.skip_if_no_wallet()
  21  
  22      def add_options(self, parser):
  23          self.add_wallet_options(parser)
  24          parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true",
  25                              help="Test double-spend of 1-confirmed transaction")
  26  
  27      def setup_network(self):
  28          # Start with split network:
  29          super().setup_network()
  30          self.disconnect_nodes(1, 2)
  31  
  32      def spend_utxo(self, utxo, outputs):
  33          inputs = [utxo]
  34          tx = self.nodes[0].createrawtransaction(inputs, outputs)
  35          tx = self.nodes[0].fundrawtransaction(tx)
  36          tx = self.nodes[0].signrawtransactionwithwallet(tx['hex'])
  37          return self.nodes[0].sendrawtransaction(tx['hex'])
  38  
  39      def run_test(self):
  40          # All nodes should start with 1,250 BTC:
  41          starting_balance = 1250
  42  
  43          # All nodes should be out of IBD.
  44          # If the nodes are not all out of IBD, that can interfere with
  45          # blockchain sync later in the test when nodes are connected, due to
  46          # timing issues.
  47          for n in self.nodes:
  48              assert n.getblockchaininfo()["initialblockdownload"] == False
  49  
  50          for i in range(3):
  51              assert_equal(self.nodes[i].getbalance(), starting_balance)
  52  
  53          # Assign coins to foo and bar addresses:
  54          node0_address_foo = self.nodes[0].getnewaddress()
  55          fund_foo_utxo = self.create_outpoints(self.nodes[0], outputs=[{node0_address_foo: 1219}])[0]
  56          fund_foo_tx = self.nodes[0].gettransaction(fund_foo_utxo['txid'])
  57          self.nodes[0].lockunspent(False, [fund_foo_utxo])
  58  
  59          node0_address_bar = self.nodes[0].getnewaddress()
  60          fund_bar_utxo = self.create_outpoints(node=self.nodes[0], outputs=[{node0_address_bar: 29}])[0]
  61          fund_bar_tx = self.nodes[0].gettransaction(fund_bar_utxo['txid'])
  62  
  63          assert_equal(self.nodes[0].getbalance(),
  64                       starting_balance + fund_foo_tx["fee"] + fund_bar_tx["fee"])
  65  
  66          # Coins are sent to node1_address
  67          node1_address = self.nodes[1].getnewaddress()
  68  
  69          # First: use raw transaction API to send 1240 BTC to node1_address,
  70          # but don't broadcast:
  71          doublespend_fee = Decimal('-.02')
  72          inputs = [fund_foo_utxo, fund_bar_utxo]
  73          change_address = self.nodes[0].getnewaddress()
  74          outputs = {}
  75          outputs[node1_address] = 1240
  76          outputs[change_address] = 1248 - 1240 + doublespend_fee
  77          rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
  78          doublespend = self.nodes[0].signrawtransactionwithwallet(rawtx)
  79          assert_equal(doublespend["complete"], True)
  80  
  81          # Create two spends using 1 50 BTC coin each
  82          txid1 = self.spend_utxo(fund_foo_utxo, {node1_address: 40})
  83          txid2 = self.spend_utxo(fund_bar_utxo, {node1_address: 20})
  84  
  85          # Have node0 mine a block:
  86          if (self.options.mine_block):
  87              self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2]))
  88  
  89          tx1 = self.nodes[0].gettransaction(txid1)
  90          tx2 = self.nodes[0].gettransaction(txid2)
  91  
  92          # Node0's balance should be starting balance, plus 50BTC for another
  93          # matured block, minus 40, minus 20, and minus transaction fees:
  94          expected = starting_balance + fund_foo_tx["fee"] + fund_bar_tx["fee"]
  95          if self.options.mine_block:
  96              expected += 50
  97          expected += tx1["amount"] + tx1["fee"]
  98          expected += tx2["amount"] + tx2["fee"]
  99          assert_equal(self.nodes[0].getbalance(), expected)
 100  
 101          if self.options.mine_block:
 102              assert_equal(tx1["confirmations"], 1)
 103              assert_equal(tx2["confirmations"], 1)
 104              # Node1's balance should be both transaction amounts:
 105              assert_equal(self.nodes[1].getbalance(), starting_balance - tx1["amount"] - tx2["amount"])
 106          else:
 107              assert_equal(tx1["confirmations"], 0)
 108              assert_equal(tx2["confirmations"], 0)
 109  
 110          # Now give doublespend and its parents to miner:
 111          self.nodes[2].sendrawtransaction(fund_foo_tx["hex"])
 112          self.nodes[2].sendrawtransaction(fund_bar_tx["hex"])
 113          doublespend_txid = self.nodes[2].sendrawtransaction(doublespend["hex"])
 114          # ... mine a block...
 115          self.generate(self.nodes[2], 1, sync_fun=self.no_op)
 116  
 117          # Reconnect the split network, and sync chain:
 118          self.connect_nodes(1, 2)
 119          self.generate(self.nodes[2], 1)  # Mine another block to make sure we sync
 120          assert_equal(self.nodes[0].gettransaction(doublespend_txid)["confirmations"], 2)
 121  
 122          # Re-fetch transaction info:
 123          tx1 = self.nodes[0].gettransaction(txid1)
 124          tx2 = self.nodes[0].gettransaction(txid2)
 125  
 126          # Both transactions should be conflicted
 127          assert_equal(tx1["confirmations"], -2)
 128          assert_equal(tx2["confirmations"], -2)
 129  
 130          # Node0's total balance should be starting balance, plus 100BTC for
 131          # two more matured blocks, minus 1240 for the double-spend, plus fees (which are
 132          # negative):
 133          expected = starting_balance + 100 - 1240 + fund_foo_tx["fee"] + fund_bar_tx["fee"] + doublespend_fee
 134          assert_equal(self.nodes[0].getbalance(), expected)
 135  
 136          # Node1's balance should be its initial balance (1250 for 25 block rewards) plus the doublespend:
 137          assert_equal(self.nodes[1].getbalance(), 1250 + 1240)
 138  
 139  
 140  if __name__ == '__main__':
 141      TxnMallTest(__file__).main()
 142