wallet_balance.py raw

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