wallet_balance.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2018-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 balance RPC methods."""
   6  from decimal import Decimal
   7  
   8  from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY
   9  from test_framework.blocktools import COINBASE_MATURITY
  10  from test_framework.test_framework import LimenkaTestFramework
  11  from test_framework.util import (
  12      assert_equal,
  13      assert_is_hash_string,
  14      assert_raises_rpc_error,
  15  )
  16  
  17  
  18  def create_transactions(node, address, amt, fees):
  19      # Create and sign raw transactions from node to address for amt.
  20      # Creates a transaction for each fee and returns an array
  21      # of the raw transactions.
  22      utxos = [u for u in node.listunspent(0) if u['spendable']]
  23  
  24      # Create transactions
  25      inputs = []
  26      ins_total = 0
  27      for utxo in utxos:
  28          inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]})
  29          ins_total += utxo['amount']
  30          if ins_total >= amt + max(fees):
  31              break
  32      # make sure there was enough utxos
  33      assert ins_total >= amt + max(fees)
  34  
  35      txs = []
  36      for fee in fees:
  37          outputs = {address: amt}
  38          # prevent 0 change output
  39          if ins_total > amt + fee:
  40              outputs[node.getrawchangeaddress()] = ins_total - amt - fee
  41          raw_tx = node.createrawtransaction(inputs, outputs, 0, True)
  42          raw_tx = node.signrawtransactionwithwallet(raw_tx)
  43          assert_equal(raw_tx['complete'], True)
  44          txs.append(raw_tx)
  45  
  46      return txs
  47  
  48  class WalletTest(LimenkaTestFramework):
  49      def add_options(self, parser):
  50          self.add_wallet_options(parser)
  51  
  52      def set_test_params(self):
  53          self.num_nodes = 2
  54          self.setup_clean_chain = True
  55          # whitelist peers to speed up tx relay / mempool sync
  56          self.noban_tx_relay = True
  57          self.extra_args = [
  58              # Limit mempool descendants as a hack to have wallet txs rejected from the mempool.
  59              # Set walletrejectlongchains=0 so the wallet still creates the transactions.
  60              ['-limitdescendantcount=3', '-walletrejectlongchains=0'],
  61              [],
  62          ]
  63  
  64      def skip_test_if_missing_module(self):
  65          self.skip_if_no_wallet()
  66  
  67      def run_test(self):
  68          if not self.options.descriptors:
  69              # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets
  70              self.nodes[0].importaddress(ADDRESS_WATCHONLY)
  71              # Check that nodes don't own any UTXOs
  72              assert_equal(len(self.nodes[0].listunspent()), 0)
  73              assert_equal(len(self.nodes[1].listunspent()), 0)
  74  
  75              self.log.info("Check that only node 0 is watching an address")
  76              assert 'watchonly' in self.nodes[0].getbalances()
  77              assert 'watchonly' not in self.nodes[1].getbalances()
  78  
  79          self.log.info("Mining blocks ...")
  80          self.generate(self.nodes[0], 1)
  81          self.generate(self.nodes[1], 1)
  82  
  83          # Verify listunspent returns immature coinbase if 'include_immature_coinbase' is set
  84          assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1)
  85          assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 0)
  86  
  87          self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, ADDRESS_WATCHONLY)
  88  
  89          # Verify listunspent returns all immature coinbases if 'include_immature_coinbase' is set
  90          # For now, only the legacy wallet will see the coinbases going to the imported 'ADDRESS_WATCHONLY'
  91          assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 1 if self.options.descriptors else 2)
  92          assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1 if self.options.descriptors else COINBASE_MATURITY + 2)
  93  
  94          if not self.options.descriptors:
  95              # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets
  96              assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50)
  97              assert_equal(self.nodes[0].getwalletinfo()['balance'], 50)
  98              assert_equal(self.nodes[1].getbalances()['mine']['trusted'], 50)
  99  
 100              assert_equal(self.nodes[0].getbalances()['watchonly']['immature'], 5000)
 101              assert 'watchonly' not in self.nodes[1].getbalances()
 102  
 103              assert_equal(self.nodes[0].getbalance(), 50)
 104              assert_equal(self.nodes[1].getbalance(), 50)
 105  
 106          self.log.info("Test getbalance with different arguments")
 107          assert_equal(self.nodes[0].getbalance("*"), 50)
 108          assert_equal(self.nodes[0].getbalance("*", 1), 50)
 109          assert_raises_rpc_error(-8, "getbalance minconf option is only currently supported if dummy is set to \"*\"", self.nodes[0].getbalance, minconf=1)
 110          assert_raises_rpc_error(-8, "getbalance minconf option is only currently supported if dummy is set to \"*\"", self.nodes[0].getbalance, minconf=0, include_watchonly=True)
 111          if not self.options.descriptors:
 112              assert_equal(self.nodes[0].getbalance("*", 1, True), 100)
 113          else:
 114              assert_equal(self.nodes[0].getbalance("*", 1, True), 50)
 115          assert_raises_rpc_error(-8, "getbalance minconf option is only currently supported if dummy is set to \"*\"", self.nodes[1].getbalance, minconf=0, include_watchonly=True)
 116  
 117          # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0.
 118          txs = create_transactions(self.nodes[0], self.nodes[1].getnewaddress(), 40, [Decimal('0.01')])
 119          self.nodes[0].sendrawtransaction(txs[0]['hex'])
 120          self.nodes[1].sendrawtransaction(txs[0]['hex'])  # sending on both nodes is faster than waiting for propagation
 121  
 122          self.sync_all()
 123          txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60, [Decimal('0.01'), Decimal('0.02')])
 124          self.nodes[1].sendrawtransaction(txs[0]['hex'])
 125          self.nodes[0].sendrawtransaction(txs[0]['hex'])  # sending on both nodes is faster than waiting for propagation
 126          self.sync_all()
 127  
 128          # First argument of getbalance must be set to "*"
 129          assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "")
 130  
 131          self.log.info("Test balances with unconfirmed inputs")
 132  
 133          # Before `test_balance()`, we have had two nodes with a balance of 50
 134          # each and then we:
 135          #
 136          # 1) Sent 40 from node A to node B with fee 0.01
 137          # 2) Sent 60 from node B to node A with fee 0.01
 138          #
 139          # Then we check the balances:
 140          #
 141          # 1) As is
 142          # 2) With transaction 2 from above with 2x the fee
 143          #
 144          # Prior to #16766, in this situation, the node would immediately report
 145          # a balance of 30 on node B as unconfirmed and trusted.
 146          #
 147          # After #16766, we show that balance as unconfirmed.
 148          #
 149          # The balance is indeed "trusted" and "confirmed" insofar as removing
 150          # the mempool transactions would return at least that much money. But
 151          # the algorithm after #16766 marks it as unconfirmed because the 'taint'
 152          # tracking of transaction trust for summing balances doesn't consider
 153          # which inputs belong to a user. In this case, the change output in
 154          # question could be "destroyed" by replace the 1st transaction above.
 155          #
 156          # The post #16766 behavior is correct; we shouldn't be treating those
 157          # funds as confirmed. If you want to rely on that specific UTXO existing
 158          # which has given you that balance, you cannot, as a third party
 159          # spending the other input would destroy that unconfirmed.
 160          #
 161          # For example, if the test transactions were:
 162          #
 163          # 1) Sent 40 from node A to node B with fee 0.01
 164          # 2) Sent 10 from node B to node A with fee 0.01
 165          #
 166          # Then our node would report a confirmed balance of 40 + 50 - 10 = 80
 167          # BTC, which is more than would be available if transaction 1 were
 168          # replaced.
 169  
 170  
 171          def test_balances(*, fee_node_1=0):
 172              # getbalances
 173              expected_balances_0 = {'mine':      {'immature':          Decimal('0E-8'),
 174                                                   'trusted':           Decimal('9.99'),  # change from node 0's send
 175                                                   'untrusted_pending': Decimal('60.0')},
 176                                     'watchonly': {'immature':          Decimal('5000'),
 177                                                   'trusted':           Decimal('50.0'),
 178                                                   'untrusted_pending': Decimal('0E-8')}}
 179              expected_balances_1 = {'mine':      {'immature':          Decimal('0E-8'),
 180                                                   'trusted':           Decimal('0E-8'),  # node 1's send had an unsafe input
 181                                                   'untrusted_pending': Decimal('30.0') - fee_node_1}}  # Doesn't include output of node 0's send since it was spent
 182              if self.options.descriptors:
 183                  del expected_balances_0["watchonly"]
 184              balances_0 = self.nodes[0].getbalances()
 185              balances_1 = self.nodes[1].getbalances()
 186              # remove lastprocessedblock keys (they will be tested later)
 187              del balances_0['lastprocessedblock']
 188              del balances_1['lastprocessedblock']
 189              assert_equal(balances_0, expected_balances_0)
 190              assert_equal(balances_1, expected_balances_1)
 191              # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions
 192              assert_equal(self.nodes[0].getbalance(), Decimal('9.99'))  # change from node 0's send
 193              assert_equal(self.nodes[1].getbalance(), Decimal('0'))  # node 1's send had an unsafe input
 194              # getbalance with '*' and minconf=0 includes unconfirmed transactions, AND untrusted transactions
 195              assert_equal(self.nodes[0].getbalance('*', 0), Decimal('69.99'))
 196              assert_equal(self.nodes[1].getbalance('*', 0), Decimal('30') - fee_node_1)
 197              # getbalance with '*' and minconf=1 includes only confirmed and sent transactions
 198              assert_equal(self.nodes[0].getbalance('*', 1), Decimal('9.99'))
 199              assert_equal(self.nodes[1].getbalance('*', 1), Decimal('-10') - fee_node_1)
 200              # getunconfirmedbalance
 201              assert_equal(self.nodes[0].getunconfirmedbalance(), Decimal('60'))  # output of node 1's spend
 202              assert_equal(self.nodes[1].getunconfirmedbalance(), Decimal('30') - fee_node_1)  # Doesn't include output of node 0's send since it was spent
 203              # getwalletinfo.unconfirmed_balance
 204              assert_equal(self.nodes[0].getwalletinfo()["unconfirmed_balance"], Decimal('60'))
 205              assert_equal(self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('30') - fee_node_1)
 206  
 207          test_balances(fee_node_1=Decimal('0.01'))
 208  
 209          # Node 1 bumps the transaction fee and resends
 210          self.nodes[1].sendrawtransaction(txs[1]['hex'])
 211          self.nodes[0].sendrawtransaction(txs[1]['hex'])  # sending on both nodes is faster than waiting for propagation
 212          self.sync_all()
 213  
 214          self.log.info("Test getbalance and getbalances.mine.untrusted_pending with conflicted unconfirmed inputs")
 215          test_balances(fee_node_1=Decimal('0.02'))
 216  
 217          self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)
 218  
 219          # balances are correct after the transactions are confirmed
 220          balance_node0 = Decimal('69.99')  # node 1's send plus change from node 0's send
 221          balance_node1 = Decimal('29.98')  # change from node 0's send
 222          assert_equal(self.nodes[0].getbalances()['mine']['trusted'], balance_node0)
 223          assert_equal(self.nodes[1].getbalances()['mine']['trusted'], balance_node1)
 224          assert_equal(self.nodes[0].getbalance(), balance_node0)
 225          assert_equal(self.nodes[1].getbalance(), balance_node1)
 226  
 227          # Send total balance away from node 1
 228          txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), Decimal('29.97'), [Decimal('0.01')])
 229          self.nodes[1].sendrawtransaction(txs[0]['hex'])
 230          self.generatetoaddress(self.nodes[1], 2, ADDRESS_WATCHONLY)
 231  
 232          # check mempool transactions count for wallet unconfirmed balance after
 233          # dynamically loading the wallet.
 234          before = self.nodes[1].getbalances()['mine']['untrusted_pending']
 235          dst = self.nodes[1].getnewaddress()
 236          self.nodes[1].unloadwallet(self.default_wallet_name)
 237          self.nodes[0].sendtoaddress(dst, 0.1)
 238          self.sync_all()
 239          self.nodes[1].loadwallet(self.default_wallet_name)
 240          after = self.nodes[1].getbalances()['mine']['untrusted_pending']
 241          assert_equal(before + Decimal('0.1'), after)
 242  
 243          # Create 3 more wallet txs, where the last is not accepted to the
 244          # mempool because it is the third descendant of the tx above
 245          for _ in range(3):
 246              # Set amount high enough such that all coins are spent by each tx
 247              txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 99)
 248  
 249          self.log.info('Check that wallet txs not in the mempool are untrusted')
 250          assert txid not in self.nodes[0].getrawmempool()
 251          assert_equal(self.nodes[0].gettransaction(txid)['trusted'], False)
 252          assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 0)
 253  
 254          self.log.info("Test replacement and reorg of non-mempool tx")
 255          tx_orig = self.nodes[0].gettransaction(txid)['hex']
 256          # Increase fee by 1 coin
 257          tx_replace = tx_orig.replace(
 258              (99 * 10**8).to_bytes(8, "little", signed=True).hex(),
 259              (98 * 10**8).to_bytes(8, "little", signed=True).hex(),
 260          )
 261          tx_replace = self.nodes[0].signrawtransactionwithwallet(tx_replace)['hex']
 262          # Total balance is given by the sum of outputs of the tx
 263          total_amount = sum([o['value'] for o in self.nodes[0].decoderawtransaction(tx_replace)['vout']])
 264          self.sync_all()
 265          self.nodes[1].sendrawtransaction(hexstring=tx_replace, maxfeerate=0)
 266  
 267          # Now confirm tx_replace
 268          block_reorg = self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)[0]
 269          assert_equal(self.nodes[0].getbalances()['mine']['trusted'], total_amount)
 270  
 271          self.log.info('Put txs back into mempool of node 1 (not node 0)')
 272          self.nodes[0].invalidateblock(block_reorg)
 273          self.nodes[1].invalidateblock(block_reorg)
 274          assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 0)  # wallet txs not in the mempool are untrusted
 275          self.generatetoaddress(self.nodes[0], 1, ADDRESS_WATCHONLY, sync_fun=self.no_op)
 276  
 277          # Now confirm tx_orig
 278          self.restart_node(1, ['-persistmempool=0'])
 279          self.connect_nodes(0, 1)
 280          self.sync_blocks()
 281          self.nodes[1].sendrawtransaction(tx_orig)
 282          self.generatetoaddress(self.nodes[1], 1, ADDRESS_WATCHONLY)
 283          assert_equal(self.nodes[0].getbalances()['mine']['trusted'], total_amount + 1)  # The reorg recovered our fee of 1 coin
 284  
 285          if not self.options.descriptors:
 286              self.log.info('Check if mempool is taken into account after import*')
 287              address = self.nodes[0].getnewaddress()
 288              privkey = self.nodes[0].dumpprivkey(address)
 289              self.nodes[0].sendtoaddress(address, 0.1)
 290              self.nodes[0].unloadwallet('')
 291              # check importaddress on fresh wallet
 292              self.nodes[0].createwallet('w1', False, True)
 293              self.nodes[0].importaddress(address)
 294              assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], 0)
 295              assert_equal(self.nodes[0].getbalances()['watchonly']['untrusted_pending'], Decimal('0.1'))
 296              self.nodes[0].importprivkey(privkey)
 297              assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], Decimal('0.1'))
 298              assert_equal(self.nodes[0].getbalances()['watchonly']['untrusted_pending'], 0)
 299              self.nodes[0].unloadwallet('w1')
 300              # check importprivkey on fresh wallet
 301              self.nodes[0].createwallet('w2', False, True)
 302              self.nodes[0].importprivkey(privkey)
 303              assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], Decimal('0.1'))
 304  
 305  
 306          # Tests the lastprocessedblock JSON object in getbalances, getwalletinfo
 307          # and gettransaction by checking for valid hex strings and by comparing
 308          # the hashes & heights between generated blocks.
 309          self.log.info("Test getbalances returns expected lastprocessedblock json object")
 310          prev_hash = self.nodes[0].getbestblockhash()
 311          prev_height = self.nodes[0].getblock(prev_hash)['height']
 312          self.generatetoaddress(self.nodes[0], 5, self.nodes[0].get_deterministic_priv_key().address)
 313          lastblock = self.nodes[0].getbalances()['lastprocessedblock']
 314          assert_is_hash_string(lastblock['hash'])
 315          assert_equal((prev_hash == lastblock['hash']), False)
 316          assert_equal(lastblock['height'], prev_height + 5)
 317  
 318          prev_hash = self.nodes[0].getbestblockhash()
 319          prev_height = self.nodes[0].getblock(prev_hash)['height']
 320          self.log.info("Test getwalletinfo returns expected lastprocessedblock json object")
 321          walletinfo = self.nodes[0].getwalletinfo()
 322          assert_equal(walletinfo['lastprocessedblock']['height'], prev_height)
 323          assert_equal(walletinfo['lastprocessedblock']['hash'], prev_hash)
 324  
 325          self.log.info("Test gettransaction returns expected lastprocessedblock json object")
 326          txid = self.nodes[1].sendtoaddress(self.nodes[1].getnewaddress(), 0.01)
 327          tx_info = self.nodes[1].gettransaction(txid)
 328          assert_equal(tx_info['lastprocessedblock']['height'], prev_height)
 329          assert_equal(tx_info['lastprocessedblock']['hash'], prev_hash)
 330  
 331  if __name__ == '__main__':
 332      WalletTest(__file__).main()
 333