wallet_change_address.py raw

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