wallet_txn_clone.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 are cloned transactions with malleated scriptsigs."""
   6  
   7  from test_framework.test_framework import LimenkaTestFramework
   8  from test_framework.util import (
   9      assert_equal,
  10  )
  11  from test_framework.messages import (
  12      COIN,
  13      tx_from_hex,
  14  )
  15  
  16  
  17  class TxnMallTest(LimenkaTestFramework):
  18      def set_test_params(self):
  19          self.num_nodes = 3
  20          self.supports_cli = False
  21  
  22      def skip_test_if_missing_module(self):
  23          self.skip_if_no_wallet()
  24  
  25      def add_options(self, parser):
  26          self.add_wallet_options(parser)
  27          parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true",
  28                              help="Test double-spend of 1-confirmed transaction")
  29          parser.add_argument("--segwit", dest="segwit", default=False, action="store_true",
  30                              help="Test behaviour with SegWit txn (which should fail)")
  31  
  32      def setup_network(self):
  33          # Start with split network:
  34          super().setup_network()
  35          self.disconnect_nodes(1, 2)
  36  
  37      def spend_utxo(self, utxo, outputs):
  38          inputs = [utxo]
  39          tx = self.nodes[0].createrawtransaction(inputs, outputs)
  40          tx = self.nodes[0].fundrawtransaction(tx)
  41          tx = self.nodes[0].signrawtransactionwithwallet(tx['hex'])
  42          return self.nodes[0].sendrawtransaction(tx['hex'])
  43  
  44      def run_test(self):
  45          if self.options.segwit:
  46              output_type = "p2sh-segwit"
  47          else:
  48              output_type = "legacy"
  49  
  50          # All nodes should start with 1,250 BTC:
  51          starting_balance = 1250
  52          for i in range(3):
  53              assert_equal(self.nodes[i].getbalance(), starting_balance)
  54  
  55          self.nodes[0].settxfee(.001)
  56  
  57          node0_address1 = self.nodes[0].getnewaddress(address_type=output_type)
  58          node0_utxo1 = self.create_outpoints(self.nodes[0], outputs=[{node0_address1: 1219}])[0]
  59          node0_tx1 = self.nodes[0].gettransaction(node0_utxo1['txid'])
  60          self.nodes[0].lockunspent(False, [node0_utxo1])
  61  
  62          node0_address2 = self.nodes[0].getnewaddress(address_type=output_type)
  63          node0_utxo2 = self.create_outpoints(self.nodes[0], outputs=[{node0_address2: 29}])[0]
  64          node0_tx2 = self.nodes[0].gettransaction(node0_utxo2['txid'])
  65  
  66          assert_equal(self.nodes[0].getbalance(),
  67                       starting_balance + node0_tx1["fee"] + node0_tx2["fee"])
  68  
  69          # Coins are sent to node1_address
  70          node1_address = self.nodes[1].getnewaddress()
  71  
  72          # Send tx1, and another transaction tx2 that won't be cloned
  73          txid1 = self.spend_utxo(node0_utxo1, {node1_address: 40})
  74          txid2 = self.spend_utxo(node0_utxo2, {node1_address: 20})
  75  
  76          # Construct a clone of tx1, to be malleated
  77          rawtx1 = self.nodes[0].getrawtransaction(txid1, 1)
  78          clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"], "sequence": rawtx1["vin"][0]["sequence"]}]
  79          clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["address"]: rawtx1["vout"][0]["value"],
  80                           rawtx1["vout"][1]["scriptPubKey"]["address"]: rawtx1["vout"][1]["value"]}
  81          clone_locktime = rawtx1["locktime"]
  82          clone_raw = self.nodes[0].createrawtransaction(clone_inputs, clone_outputs, clone_locktime)
  83  
  84          # createrawtransaction randomizes the order of its outputs, so swap them if necessary.
  85          clone_tx = tx_from_hex(clone_raw)
  86          if (rawtx1["vout"][0]["value"] == 40 and clone_tx.vout[0].nValue != 40*COIN or rawtx1["vout"][0]["value"] != 40 and clone_tx.vout[0].nValue == 40*COIN):
  87              (clone_tx.vout[0], clone_tx.vout[1]) = (clone_tx.vout[1], clone_tx.vout[0])
  88  
  89          # Use a different signature hash type to sign.  This creates an equivalent but malleated clone.
  90          # Don't send the clone anywhere yet
  91          tx1_clone = self.nodes[0].signrawtransactionwithwallet(clone_tx.serialize().hex(), None, "ALL|ANYONECANPAY")
  92          assert_equal(tx1_clone["complete"], True)
  93  
  94          # Have node0 mine a block, if requested:
  95          if (self.options.mine_block):
  96              self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2]))
  97  
  98          tx1 = self.nodes[0].gettransaction(txid1)
  99          tx2 = self.nodes[0].gettransaction(txid2)
 100  
 101          # Node0's balance should be starting balance, plus 50BTC for another
 102          # matured block, minus tx1 and tx2 amounts, and minus transaction fees:
 103          expected = starting_balance + node0_tx1["fee"] + node0_tx2["fee"]
 104          if self.options.mine_block:
 105              expected += 50
 106          expected += tx1["amount"] + tx1["fee"]
 107          expected += tx2["amount"] + tx2["fee"]
 108          assert_equal(self.nodes[0].getbalance(), expected)
 109  
 110          if self.options.mine_block:
 111              assert_equal(tx1["confirmations"], 1)
 112              assert_equal(tx2["confirmations"], 1)
 113          else:
 114              assert_equal(tx1["confirmations"], 0)
 115              assert_equal(tx2["confirmations"], 0)
 116  
 117          # Send clone and its parent to miner
 118          self.nodes[2].sendrawtransaction(node0_tx1["hex"])
 119          txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"])
 120          if self.options.segwit:
 121              assert_equal(txid1, txid1_clone)
 122              return
 123  
 124          # ... mine a block...
 125          self.generate(self.nodes[2], 1, sync_fun=self.no_op)
 126  
 127          # Reconnect the split network, and sync chain:
 128          self.connect_nodes(1, 2)
 129          self.nodes[2].sendrawtransaction(node0_tx2["hex"])
 130          self.nodes[2].sendrawtransaction(tx2["hex"])
 131          self.generate(self.nodes[2], 1)  # Mine another block to make sure we sync
 132  
 133          # Re-fetch transaction info:
 134          tx1 = self.nodes[0].gettransaction(txid1)
 135          tx1_clone = self.nodes[0].gettransaction(txid1_clone)
 136          tx2 = self.nodes[0].gettransaction(txid2)
 137  
 138          # Verify expected confirmations
 139          assert_equal(tx1["confirmations"], -2)
 140          assert_equal(tx1_clone["confirmations"], 2)
 141          assert_equal(tx2["confirmations"], 1)
 142  
 143          # Check node0's total balance; should be same as before the clone, + 100 BTC for 2 matured,
 144          # less possible orphaned matured subsidy
 145          expected += 100
 146          if (self.options.mine_block):
 147              expected -= 50
 148          assert_equal(self.nodes[0].getbalance(), expected)
 149  
 150  
 151  if __name__ == '__main__':
 152      TxnMallTest(__file__).main()
 153