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