wallet_change_address.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2023-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 wallet change address selection"""
   6  
   7  import re
   8  
   9  from test_framework.blocktools import COINBASE_MATURITY
  10  from test_framework.test_framework import BitcoinTestFramework
  11  from test_framework.util import (
  12      assert_equal,
  13  )
  14  
  15  
  16  class WalletChangeAddressTest(BitcoinTestFramework):
  17      def set_test_params(self):
  18          self.setup_clean_chain = True
  19          self.num_nodes = 3
  20          # discardfee is used to make change outputs less likely in the change_pos test
  21          self.extra_args = [
  22              [],
  23              ["-discardfee=1"],
  24              ["-avoidpartialspends", "-discardfee=1"]
  25          ]
  26  
  27      def skip_test_if_missing_module(self):
  28          self.skip_if_no_wallet()
  29  
  30      def assert_change_index(self, node, tx, index):
  31          change_index = None
  32          for vout in tx["vout"]:
  33              info = node.getaddressinfo(vout["scriptPubKey"]["address"])
  34              if (info["ismine"] and info["ischange"]):
  35                  change_index = int(re.findall(r'\d+', info["hdkeypath"])[-1])
  36                  break
  37          assert_equal(change_index, index)
  38  
  39      def assert_change_pos(self, wallet, tx, pos):
  40          change_pos = None
  41          for index, output in enumerate(tx["vout"]):
  42              info = wallet.getaddressinfo(output["scriptPubKey"]["address"])
  43              if (info["ismine"] and info["ischange"]):
  44                  change_pos = index
  45                  break
  46          assert_equal(change_pos, pos)
  47  
  48      def run_test(self):
  49          self.log.info("Setting up")
  50          # Mine some coins
  51          self.generate(self.nodes[0], COINBASE_MATURITY + 1)
  52  
  53          # Get some addresses from the two nodes
  54          addr1 = [self.nodes[1].getnewaddress() for _ in range(3)]
  55          addr2 = [self.nodes[2].getnewaddress() for _ in range(3)]
  56          addrs = addr1 + addr2
  57  
  58          # Send 1 + 0.5 coin to each address
  59          [self.nodes[0].sendtoaddress(addr, 1.0) for addr in addrs]
  60          [self.nodes[0].sendtoaddress(addr, 0.5) for addr in addrs]
  61          self.generate(self.nodes[0], 1)
  62  
  63          for i in range(20):
  64              for n in [1, 2]:
  65                  self.log.debug(f"Send transaction from node {n}: expected change index {i}")
  66                  txid = self.nodes[n].sendtoaddress(self.nodes[0].getnewaddress(), 0.2)
  67                  tx = self.nodes[n].getrawtransaction(txid, True)
  68                  # find the change output and ensure that expected change index was used
  69                  self.assert_change_index(self.nodes[n], tx, i)
  70  
  71          # Start next test with fresh wallets and new coins
  72          self.nodes[1].createwallet("w1")
  73          self.nodes[2].createwallet("w2")
  74          w1 = self.nodes[1].get_wallet_rpc("w1")
  75          w2 = self.nodes[2].get_wallet_rpc("w2")
  76          addr1 = w1.getnewaddress()
  77          addr2 = w2.getnewaddress()
  78          self.nodes[0].sendtoaddress(addr1, 3.0)
  79          self.nodes[0].sendtoaddress(addr1, 0.1)
  80          self.nodes[0].sendtoaddress(addr2, 3.0)
  81          self.nodes[0].sendtoaddress(addr2, 0.1)
  82          self.generate(self.nodes[0], 1)
  83  
  84          sendTo1 = self.nodes[0].getnewaddress()
  85          sendTo2 = self.nodes[0].getnewaddress()
  86          sendTo3 = self.nodes[0].getnewaddress()
  87  
  88          # The avoid partial spends wallet will always create a change output
  89          node = self.nodes[2]
  90          res = w2.send({sendTo1: "1.0", sendTo2: "1.0", sendTo3: "0.9999"}, options={"change_position": 0})
  91          tx = node.getrawtransaction(res["txid"], True)
  92          self.assert_change_pos(w2, tx, 0)
  93  
  94          # The default wallet will internally create a tx without change first,
  95          # then create a second candidate using APS that requires a change output.
  96          # Ensure that the user-configured change position is kept
  97          node = self.nodes[1]
  98          res = w1.send({sendTo1: "1.0", sendTo2: "1.0", sendTo3: "0.9999"}, options={"change_position": 0})
  99          tx = node.getrawtransaction(res["txid"], True)
 100          # If the wallet ignores the user's change_position there is still a 25%
 101          # that the random change position passes the test
 102          self.assert_change_pos(w1, tx, 0)
 103  
 104  if __name__ == '__main__':
 105      WalletChangeAddressTest(__file__).main()
 106