wallet_taproot.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2021-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 generation and spending of P2TR addresses."""
   6  
   7  import random
   8  import uuid
   9  
  10  from decimal import Decimal
  11  from test_framework.address import output_key_to_p2tr
  12  from test_framework.key import H_POINT
  13  from test_framework.test_framework import LimenkaTestFramework
  14  from test_framework.util import assert_equal
  15  from test_framework.descriptors import descsum_create
  16  from test_framework.script import (
  17      CScript,
  18      MAX_PUBKEYS_PER_MULTI_A,
  19      OP_CHECKSIG,
  20      OP_CHECKSIGADD,
  21      OP_NUMEQUAL,
  22      taproot_construct,
  23  )
  24  from test_framework.segwit_addr import encode_segwit_address
  25  
  26  # xprvs/xpubs, and m/* derived x-only pubkeys (created using independent implementation)
  27  KEYS = [
  28      {
  29          "xprv": "tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV",
  30          "xpub": "tpubD6NzVbkrYhZ4XqNGAWGWSzmxGWFwVjVTjZxh2fioKbVYi7Jx8fdbprVWsdW7mHwqjchBVas8TLZG4Xwuz4RKU4iaCqiCvoSkFCzQptqk5Y1",
  31          "pubs": [
  32              "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18",
  33              "a30253b018ea6fca966135bf7dd8026915427f24ccf10d4e03f7870f4128569b",
  34              "a61e5749f2f3db9dc871d7b187e30bfd3297eea2557e9be99897ea8ff7a29a21",
  35              "8110cf482f66dc37125e619d73075af932521724ffc7108309e88f361efe8c8a",
  36          ]
  37      },
  38      {
  39          "xprv": "tprv8ZgxMBicQKsPe98QUPieXy5KFPVjuZNpcC9JY7K7buJEm8nWvJogK4kTda7eLjK9U4PnMNbSjEkpjDJazeBZ4rhYNYD7N6GEdaysj1AYSb5",
  40          "xpub": "tpubD6NzVbkrYhZ4XcACN3PEwNjRpR1g4tZjBVk5pdMR2B6dbd3HYhdGVZNKofAiFZd9okBserZvv58A6tBX4pE64UpXGNTSesfUW7PpW36HuKz",
  41          "pubs": [
  42              "f95886b02a84928c5c15bdca32784993105f73de27fa6ad8c1a60389b999267c",
  43              "71522134160685eb779857033bfc84c7626f13556154653a51dd42619064e679",
  44              "48957b4158b2c5c3f4c000f51fd2cf0fd5ff8868ebfb194256f5e9131fc74bd8",
  45              "086dda8139b3a84944010648d2b674b70447be3ae59322c09a4907bc80be62c1",
  46          ]
  47      },
  48      {
  49          "xprv": "tprv8ZgxMBicQKsPe3ZJmcj9aJ2EPZJYYCh6Lp3v82p75wspgaXmtDZ2RBtkAtWcGnW2VQDzMHQPBkCKMoYTqh1RfJKjv4PcmWVR7KqTpjsdboN",
  50          "xpub": "tpubD6NzVbkrYhZ4XWb6fGPjyhgLxapUhXszv7ehQYrQWDgDX4nYWcNcbgWcM2RhYo9s2mbZcfZJ8t5LzYcr24FK79zVybsw5Qj3Rtqug8jpJMy",
  51          "pubs": [
  52              "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7",
  53              "8a104c54cd34acba60c97dd8f1f7abc89ba9587afd88dc928e91aca7b1c50d20",
  54              "13ba6b252a4eb5ef31d39cb521724cdab19a698323f5c17093f28fb1821d052f",
  55              "f6c2b4863fd5ba1ba09e3a890caed8b75ffbe013ebab31a06ab87cd6f72506af",
  56          ]
  57      },
  58      {
  59          "xprv": "tprv8ZgxMBicQKsPdKziibn63Rm6aNzp7dSjDnufZMStXr71Huz7iihCRpbZZZ6Voy5HyuHCWx6foHMipzMzUq4tZrtkZ24DJwz5EeNWdsuwX5h",
  60          "xpub": "tpubD6NzVbkrYhZ4Wo2WcFSgSqRD9QWkGxddo6WSqsVBx7uQ8QEtM7WncKDRjhFEexK119NigyCsFygA4b7sAPQxqebyFGAZ9XVV1BtcgNzbCRR",
  61          "pubs": [
  62              "03a669ea926f381582ec4a000b9472ba8a17347f5fb159eddd4a07036a6718eb",
  63              "bbf56b14b119bccafb686adec2e3d2a6b51b1626213590c3afa815d1fd36f85d",
  64              "2994519e31bbc238a07d82f85c9832b831705d2ee4a2dbb477ecec8a3f570fe5",
  65              "68991b5c139a4c479f8c89d6254d288c533aefc0c5b91fac6c89019c4de64988",
  66          ]
  67      },
  68      {
  69          "xprv": "tprv8ZgxMBicQKsPen4PGtDwURYnCtVMDejyE8vVwMGhQWfVqB2FBPdekhTacDW4vmsKTsgC1wsncVqXiZdX2YFGAnKoLXYf42M78fQJFzuDYFN",
  70          "xpub": "tpubD6NzVbkrYhZ4YF6BAXtXsqCtmv1HNyvsoSXHDsJzpnTtffH1onTEwC5SnLzCHPKPebh2i7Gxvi9kJNADcpuSmH8oM3rCYcHVtdXHjpYoKnX",
  71          "pubs": [
  72              "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247",
  73              "c8558b7caf198e892032d91f1a48ee9bdc25462b83b4d0ac62bb7fb2a0df630e",
  74              "8a4bcaba0e970685858d133a4d0079c8b55bbc755599e212285691eb779ce3dc",
  75              "b0d68ada13e0d954b3921b88160d4453e9c151131c2b7c724e08f538a666ceb3",
  76          ]
  77      },
  78      {
  79          "xprv": "tprv8ZgxMBicQKsPd91vCgRmbzA13wyip2RimYeVEkAyZvsEN5pUSB3T43SEBxPsytkxb42d64W2EiRE9CewpJQkzR8HKHLV8Uhk4dMF5yRPaTv",
  80          "xpub": "tpubD6NzVbkrYhZ4Wc3i6L6N1Pp7cyVeyMcdLrFGXGDGzCfdCa5F4Zs3EY46N72Ws8QDEUYBVwXfDfda2UKSseSdU1fsBegJBhGCZyxkf28bkQ6",
  81          "pubs": [
  82              "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744",
  83              "8e971b781b7ce7ab742d80278f2dfe7dd330f3efd6d00047f4a2071f2e7553cb",
  84              "b811d66739b9f07435ccda907ec5cd225355321c35e0a7c7791232f24cf10632",
  85              "4cd27a5552c272bc80ba544e9cc6340bb906969f5e7a1510b6cef9592683fbc9",
  86          ]
  87      },
  88      {
  89          "xprv": "tprv8ZgxMBicQKsPdEhLRxxwzTv2t18j7ruoffPeqAwVA2qXJ2P66RaMZLUWQ85SjoA7xPxdSgCB9UZ72m65qbnaLPtFTfHVP3MEmkpZk1Bv8RT",
  90          "xpub": "tpubD6NzVbkrYhZ4Whj8KcdYPsa9T2efHC6iExzS7gynaJdv8WdripPwjq6NaH5gQJGrLmvUwHY1smhiakUosXNDTEa6qfKUQdLKV6DJBre6XvQ",
  91          "pubs": [
  92              "d0c19def28bb1b39451c1a814737615983967780d223b79969ba692182c6006b",
  93              "cb1d1b1dc62fec1894d4c3d9a1b6738e5ff9c273a64f74e9ab363095f45e9c47",
  94              "245be588f41acfaeb9481aa132717db56ee1e23eb289729fe2b8bde8f9a00830",
  95              "5bc4ad6d6187fa82728c85a073b428483295288f8aef5722e47305b5872f7169",
  96          ]
  97      },
  98      {
  99          "xprv": "tprv8ZgxMBicQKsPcxbqxzcMAwQpiCD8x6qaZEJTxdKxw4w9GuMzDACTD9yhEsHGfqQcfYX4LivosLDDngTykYEp9JnTdcqY7cHqU8PpeFFKyV3",
 100          "xpub": "tpubD6NzVbkrYhZ4WRddreGwaM4wHDj57S2V8XuFF9NGMLjY7PckqZ23PebZR1wGA4w84uX2vZphdZVsnREjij1ibYjEBTaTVQCEZCLs4xUDapx",
 101          "pubs": [
 102              "065cc1b92bd99e5a3e626e8296a366b2d132688eb43aea19bc14fd8f43bf07fb",
 103              "5b95633a7dda34578b6985e6bfd85d83ec38b7ded892a9b74a3d899c85890562",
 104              "dc86d434b9a34495c8e845b969d51f80d19a8df03b400353ffe8036a0c22eb60",
 105              "06c8ffde238745b29ae8a97ae533e1f3edf214bba6ec58b5e7b9451d1d61ec19",
 106          ]
 107      },
 108      {
 109          "xprv": "tprv8ZgxMBicQKsPe6zLoU8MTTXgsdJVNBErrYGpoGwHf5VGvwUzdNc7NHeCSzkJkniCxBhZWujXjmD4HZmBBrnr3URgJjM6GxRgMmEhLdqNTWG",
 110          "xpub": "tpubD6NzVbkrYhZ4Xa28h7nwrsBoSepRXWRmRqsc5nyb5MHfmRjmFmRhYnG4d9dC7uxixN5AfsEv1Lz3mCAuWvERyvPgKozHUVjfo8EG6foJGy7",
 111          "pubs": [
 112              "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b",
 113              "939365e0359ff6bc6f6404ee220714c5d4a0d1e36838b9e2081ede217674e2ba",
 114              "4e8767edcf7d3d90258cfbbea01b784f4d2de813c4277b51279cf808bac410a2",
 115              "d42a2c280940bfc6ede971ae72cde2e1df96c6da7dab06a132900c6751ade208",
 116          ]
 117      },
 118      {
 119          "xprv": "tprv8ZgxMBicQKsPeB5o5oCsN2dVxM2mtJiYERQEBRc4JNwC1DFGYaEdNkmh8jJYVPU76YhkFoRoWTdh1p3yQGykG8TfDW34dKgrgSx28gswUyL",
 120          "xpub": "tpubD6NzVbkrYhZ4Xe7aySsTmSHcXNYi3duSoj11TweMiejaqhW3Ay4DZFPZJses4sfpk4b9VHRhn8v4cKTMjugMM3hqXcqSSmRdiW8QvASXjfY",
 121          "pubs": [
 122              "e360564b2e0e8d06681b6336a29d0750210e8f34afd9afb5e6fd5fe6dba26c81",
 123              "76b4900f00a1dcce463b6d8e02b768518fce4f9ecd6679a13ad78ea1e4815ad3",
 124              "5575556e263c8ed52e99ab02147cc05a738869afe0039911b5a60a780f4e43d2",
 125              "593b00e2c8d4bd6dda0fd9e238888acf427bb4e128887fd5a40e0e9da78cbc01",
 126          ]
 127      },
 128      {
 129          "xprv": "tprv8ZgxMBicQKsPfEH6jHemkGDjZRnAaKFJVGH8pQU638E6SdbX9hxit1tK2sfFPfL6KS7v8FfUKxstbfEpzSymbdfBM9Y5UkrxErF9fJaKLK3",
 130          "xpub": "tpubD6NzVbkrYhZ4YhJtcwKN9fsr8TJ6jeSD4Zsv6vWPTQ2VH7rHn6nK4WWBCzKK7FkdVVwm3iztCU1UmStY4hX6gRbBmp9UzK9C59dQEzeXS12",
 131          "pubs": [
 132              "7631cacec3343052d87ef4d0065f61dde82d7d2db0c1cc02ef61ef3c982ea763",
 133              "c05e44a9e735d1b1bef62e2c0d886e6fb4923b2649b67828290f5cacc51c71b7",
 134              "b33198b20701afe933226c92fd0e3d51d3f266f1113d864dbd026ae3166ef7f2",
 135              "f99643ac3f4072ee4a949301e86963a9ca0ad57f2ef29f6b84fda037d7cac85b",
 136          ]
 137      },
 138      {
 139          "xprv": "tprv8ZgxMBicQKsPdNWU38dT6aGxtqJR4oYS5kPpLVBcuKiiu7gqTYqMMqhUG6DP7pPahzPQu36sWSmeLCP1C4AwqcR5FX2RyRoZfd4B8pAnSdX",
 140          "xpub": "tpubD6NzVbkrYhZ4WqYFvnJ3Vyw5TrpME8jLf3zbd1DvKbX7jbwc5wewYLKLSFRzZWV6hZj7XhsXAy7fhE5jB25DiWyNM3ztXbsXHRVCrp5BiPY",
 141          "pubs": [
 142              "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7",
 143              "83df59d0a5c951cdd62b7ab225a62079f48d2a333a86e66c35420d101446e92e",
 144              "2a654bf234d819055312f9ca03fad5836f9163b09cdd24d29678f694842b874a",
 145              "aa0334ab910047387c912a21ec0dab806a47ffa38365060dbc5d47c18c6e66e7",
 146          ]
 147      },
 148      {
 149          "xprv": "tprv8mGPkMVz5mZuJDnC2NjjAv7E9Zqa5LCgX4zawbZu5nzTtLb5kGhPwycX4H1gtW1f5ZdTKTNtQJ61hk71F2TdcQ93EFDTpUcPBr98QRji615",
 150          "xpub": "tpubDHxRtmYEE9FaBgoyv2QKaKmLibMWEfPb6NbNE7cCW4nripqrNfWz8UEPEPbHCrakwLvwFfsqoaf4pjX4gWStp4nECRf1QwBKPkLqnY8pHbj",
 151          "pubs": [
 152              "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3",
 153              "b2749b74d51a78f5fe3ebb3a7c0ff266a468cade143dfa265c57e325177edf00",
 154              "6b8747a6bbe4440d7386658476da51f6e49a220508a7ec77fe7bccc3e7baa916",
 155              "4674bf4d9ebbe01bf0aceaca2472f63198655ecf2df810f8d69b38421972318e",
 156          ]
 157      }
 158  ]
 159  
 160  
 161  def key(hex_key):
 162      """Construct an x-only pubkey from its hex representation."""
 163      return bytes.fromhex(hex_key)
 164  
 165  def pk(hex_key):
 166      """Construct a script expression for taproot_construct for pk(hex_key)."""
 167      return (None, CScript([bytes.fromhex(hex_key), OP_CHECKSIG]))
 168  
 169  def multi_a(k, hex_keys, sort=False):
 170      """Construct a script expression for taproot_construct for a multi_a script."""
 171      xkeys = [bytes.fromhex(hex_key) for hex_key in hex_keys]
 172      if sort:
 173          xkeys.sort()
 174      ops = [xkeys[0], OP_CHECKSIG]
 175      for i in range(1, len(hex_keys)):
 176          ops += [xkeys[i], OP_CHECKSIGADD]
 177      ops += [k, OP_NUMEQUAL]
 178      return (None, CScript(ops))
 179  
 180  def compute_taproot_address(pubkey, scripts):
 181      """Compute the address for a taproot output with given inner key and scripts."""
 182      return output_key_to_p2tr(taproot_construct(pubkey, scripts).output_pubkey)
 183  
 184  def compute_raw_taproot_address(pubkey):
 185      return encode_segwit_address("bcrt", 1, pubkey)
 186  
 187  class WalletTaprootTest(LimenkaTestFramework):
 188      """Test generation and spending of P2TR address outputs."""
 189  
 190      def add_options(self, parser):
 191          self.add_wallet_options(parser, legacy=False)
 192  
 193      def set_test_params(self):
 194          self.num_nodes = 2
 195          self.setup_clean_chain = True
 196          self.extra_args = [['-keypool=100'], ['-keypool=100']]
 197          for ea in self.extra_args:
 198              ea.append('-addresstype=bech32m')
 199          self.supports_cli = False
 200  
 201      def skip_test_if_missing_module(self):
 202          self.skip_if_no_wallet()
 203          self.skip_if_no_sqlite()
 204  
 205      def setup_network(self):
 206          self.setup_nodes()
 207  
 208      def init_wallet(self, *, node):
 209          pass
 210  
 211      @staticmethod
 212      def make_desc(pattern, privmap, keys, pub_only = False):
 213          pat = pattern.replace("$H", H_POINT)
 214          for i in range(len(privmap)):
 215              if privmap[i] and not pub_only:
 216                  pat = pat.replace("$%i" % (i + 1), keys[i]['xprv'])
 217              else:
 218                  pat = pat.replace("$%i" % (i + 1), keys[i]['xpub'])
 219          return descsum_create(pat)
 220  
 221      @staticmethod
 222      def make_addr(treefn, keys, i):
 223          args = []
 224          for j in range(len(keys)):
 225              args.append(keys[j]['pubs'][i])
 226          tree = treefn(*args)
 227          if isinstance(tree, tuple):
 228              return compute_taproot_address(*tree)
 229          if isinstance(tree, bytes):
 230              return compute_raw_taproot_address(tree)
 231          assert False
 232  
 233      def do_test_addr(self, comment, pattern, privmap, treefn, keys):
 234          self.log.info("Testing %s address derivation" % comment)
 235  
 236          # Create wallets
 237          wallet_uuid = uuid.uuid4().hex
 238          self.nodes[0].createwallet(wallet_name=f"privs_tr_enabled_{wallet_uuid}", descriptors=True, blank=True)
 239          self.nodes[0].createwallet(wallet_name=f"pubs_tr_enabled_{wallet_uuid}", descriptors=True, blank=True, disable_private_keys=True)
 240          self.nodes[0].createwallet(wallet_name=f"addr_gen_{wallet_uuid}", descriptors=True, disable_private_keys=True, blank=True)
 241          privs_tr_enabled = self.nodes[0].get_wallet_rpc(f"privs_tr_enabled_{wallet_uuid}")
 242          pubs_tr_enabled = self.nodes[0].get_wallet_rpc(f"pubs_tr_enabled_{wallet_uuid}")
 243          addr_gen = self.nodes[0].get_wallet_rpc(f"addr_gen_{wallet_uuid}")
 244  
 245          desc = self.make_desc(pattern, privmap, keys, False)
 246          desc_pub = self.make_desc(pattern, privmap, keys, True)
 247          assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub)
 248          result = addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}])
 249          assert result[0]['success']
 250          address_type = "bech32m" if "tr" in pattern else "bech32"
 251          for i in range(4):
 252              addr_g = addr_gen.getnewaddress(address_type=address_type)
 253              if treefn is not None:
 254                  addr_r = self.make_addr(treefn, keys, i)
 255                  assert_equal(addr_g, addr_r)
 256              desc_a = addr_gen.getaddressinfo(addr_g)['desc']
 257              if desc.startswith("tr("):
 258                  assert desc_a.startswith("tr(")
 259              rederive = self.nodes[1].deriveaddresses(desc_a)
 260              assert_equal(len(rederive), 1)
 261              assert_equal(rederive[0], addr_g)
 262  
 263          # tr descriptors can be imported
 264          result = privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
 265          assert result[0]['success']
 266          result = pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
 267          assert result[0]["success"]
 268  
 269          # Cleanup
 270          privs_tr_enabled.unloadwallet()
 271          pubs_tr_enabled.unloadwallet()
 272          addr_gen.unloadwallet()
 273  
 274      def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
 275          self.log.info("Testing %s through sendtoaddress" % comment)
 276  
 277          # Create wallets
 278          wallet_uuid = uuid.uuid4().hex
 279          self.nodes[0].createwallet(wallet_name=f"rpc_online_{wallet_uuid}", descriptors=True, blank=True)
 280          rpc_online = self.nodes[0].get_wallet_rpc(f"rpc_online_{wallet_uuid}")
 281  
 282          desc_pay = self.make_desc(pattern, privmap, keys_pay)
 283          desc_change = self.make_desc(pattern, privmap, keys_change)
 284          desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
 285          desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
 286          assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
 287          assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
 288          result = rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
 289          assert result[0]['success']
 290          result = rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
 291          assert result[0]['success']
 292          address_type = "bech32m" if "tr" in pattern else "bech32"
 293          for i in range(4):
 294              addr_g = rpc_online.getnewaddress(address_type=address_type)
 295              if treefn is not None:
 296                  addr_r = self.make_addr(treefn, keys_pay, i)
 297                  assert_equal(addr_g, addr_r)
 298              boring_balance = int(self.boring.getbalance() * 100000000)
 299              to_amnt = random.randrange(1000000, boring_balance)
 300              self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
 301              self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 302              test_balance = int(rpc_online.getbalance() * 100000000)
 303              ret_amnt = random.randrange(100000, test_balance)
 304              # Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
 305              res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)
 306              self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 307              assert rpc_online.gettransaction(res)["confirmations"] > 0
 308  
 309          # Cleanup
 310          txid = rpc_online.sendall(recipients=[self.boring.getnewaddress()], fee_rate=200)["txid"]
 311          self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 312          assert rpc_online.gettransaction(txid)["confirmations"] > 0
 313          rpc_online.unloadwallet()
 314  
 315      def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
 316          self.log.info("Testing %s through PSBT" % comment)
 317  
 318          # Create wallets
 319          wallet_uuid = uuid.uuid4().hex
 320          self.nodes[0].createwallet(wallet_name=f"psbt_online_{wallet_uuid}", descriptors=True, disable_private_keys=True, blank=True)
 321          self.nodes[1].createwallet(wallet_name=f"psbt_offline_{wallet_uuid}", descriptors=True, blank=True)
 322          self.nodes[1].createwallet(f"key_only_wallet_{wallet_uuid}", descriptors=True, blank=True)
 323          psbt_online = self.nodes[0].get_wallet_rpc(f"psbt_online_{wallet_uuid}")
 324          psbt_offline = self.nodes[1].get_wallet_rpc(f"psbt_offline_{wallet_uuid}")
 325          key_only_wallet = self.nodes[1].get_wallet_rpc(f"key_only_wallet_{wallet_uuid}")
 326  
 327          desc_pay = self.make_desc(pattern, privmap, keys_pay, False)
 328          desc_change = self.make_desc(pattern, privmap, keys_change, False)
 329          desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
 330          desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
 331          assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
 332          assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
 333          result = psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])
 334          assert result[0]['success']
 335          result = psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])
 336          assert result[0]['success']
 337          result = psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
 338          assert result[0]['success']
 339          result = psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
 340          assert result[0]['success']
 341          for key in keys_pay + keys_change:
 342              result = key_only_wallet.importdescriptors([{"desc": descsum_create(f"wpkh({key['xprv']}/*)"), "timestamp":"now"}])
 343              assert result[0]["success"]
 344          address_type = "bech32m" if "tr" in pattern else "bech32"
 345          for i in range(4):
 346              addr_g = psbt_online.getnewaddress(address_type=address_type)
 347              if treefn is not None:
 348                  addr_r = self.make_addr(treefn, keys_pay, i)
 349                  assert_equal(addr_g, addr_r)
 350              boring_balance = int(self.boring.getbalance() * 100000000)
 351              to_amnt = random.randrange(1000000, boring_balance)
 352              self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
 353              self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 354              test_balance = int(psbt_online.getbalance() * 100000000)
 355              ret_amnt = random.randrange(100000, test_balance)
 356              # Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
 357              psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": address_type})['psbt']
 358              res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
 359              for wallet in [psbt_offline, key_only_wallet]:
 360                  res = wallet.walletprocesspsbt(psbt=psbt, finalize=False)
 361  
 362                  decoded = wallet.decodepsbt(res["psbt"])
 363                  if pattern.startswith("tr("):
 364                      for psbtin in decoded["inputs"]:
 365                          assert "non_witness_utxo" not in psbtin
 366                          assert "witness_utxo" in psbtin
 367                          assert "taproot_internal_key" in psbtin
 368                          assert "taproot_bip32_derivs" in psbtin
 369                          assert "taproot_key_path_sig" in psbtin or "taproot_script_path_sigs" in psbtin
 370                          if "taproot_script_path_sigs" in psbtin:
 371                              assert "taproot_merkle_root" in psbtin
 372                              assert "taproot_scripts" in psbtin
 373  
 374                  rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
 375                  res = self.nodes[0].testmempoolaccept([rawtx])
 376                  assert res[0]["allowed"]
 377  
 378              txid = self.nodes[0].sendrawtransaction(rawtx)
 379              self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 380              assert psbt_online.gettransaction(txid)['confirmations'] > 0
 381  
 382          # Cleanup
 383          psbt = psbt_online.sendall(recipients=[self.boring.getnewaddress()], psbt=True, conf_target=1, estimate_mode='conservative')["psbt"]
 384          res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
 385          rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
 386          txid = self.nodes[0].sendrawtransaction(rawtx)
 387          self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
 388          assert psbt_online.gettransaction(txid)['confirmations'] > 0
 389          psbt_online.unloadwallet()
 390          psbt_offline.unloadwallet()
 391  
 392      def do_test(self, comment, pattern, privmap, treefn):
 393          nkeys = len(privmap)
 394          keys = random.sample(KEYS, nkeys * 4)
 395          self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys])
 396          self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys])
 397          self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
 398  
 399      def run_test(self):
 400          self.nodes[0].createwallet(wallet_name="boring")
 401          self.boring = self.nodes[0].get_wallet_rpc("boring")
 402  
 403          self.log.info("Mining blocks...")
 404          gen_addr = self.boring.getnewaddress()
 405          self.generatetoaddress(self.nodes[0], 101, gen_addr, sync_fun=self.no_op)
 406  
 407          self.do_test(
 408              "tr(XPRV)",
 409              "tr($1/*)",
 410              [True],
 411              lambda k1: (key(k1), [])
 412          )
 413          self.do_test(
 414              "tr(H,XPRV)",
 415              "tr($H,pk($1/*))",
 416              [True],
 417              lambda k1: (key(H_POINT), [pk(k1)])
 418          )
 419          self.do_test(
 420              "wpkh(XPRV)",
 421              "wpkh($1/*)",
 422              [True],
 423              None
 424          )
 425          self.do_test(
 426              "tr(XPRV,{H,{H,XPUB}})",
 427              "tr($1/*,{pk($H),{pk($H),pk($2/*)}})",
 428              [True, False],
 429              lambda k1, k2: (key(k1), [pk(H_POINT), [pk(H_POINT), pk(k2)]])
 430          )
 431          self.do_test(
 432              "wsh(multi(1,XPRV,XPUB))",
 433              "wsh(multi(1,$1/*,$2/*))",
 434              [True, False],
 435              None
 436          )
 437          self.do_test(
 438              "tr(XPRV,{XPUB,XPUB})",
 439              "tr($1/*,{pk($2/*),pk($2/*)})",
 440              [True, False],
 441              lambda k1, k2: (key(k1), [pk(k2), pk(k2)])
 442          )
 443          self.do_test(
 444              "tr(XPRV,{{XPUB,H},{H,XPUB}})",
 445              "tr($1/*,{{pk($2/*),pk($H)},{pk($H),pk($2/*)}})",
 446              [True, False],
 447              lambda k1, k2: (key(k1), [[pk(k2), pk(H_POINT)], [pk(H_POINT), pk(k2)]])
 448          )
 449          self.do_test(
 450              "tr(XPUB,{{H,{H,XPUB}},{H,{H,{H,XPRV}}}})",
 451              "tr($1/*,{{pk($H),{pk($H),pk($2/*)}},{pk($H),{pk($H),{pk($H),pk($3/*)}}}})",
 452              [False, False, True],
 453              lambda k1, k2, k3: (key(k1), [[pk(H_POINT), [pk(H_POINT), pk(k2)]], [pk(H_POINT), [pk(H_POINT), [pk(H_POINT), pk(k3)]]]])
 454          )
 455          self.do_test(
 456              "tr(XPRV,{XPUB,{{XPUB,{H,H}},{{H,H},XPUB}}})",
 457              "tr($1/*,{pk($2/*),{{pk($2/*),{pk($H),pk($H)}},{{pk($H),pk($H)},pk($2/*)}}})",
 458              [True, False],
 459              lambda k1, k2: (key(k1), [pk(k2), [[pk(k2), [pk(H_POINT), pk(H_POINT)]], [[pk(H_POINT), pk(H_POINT)], pk(k2)]]])
 460          )
 461          self.do_test(
 462              "tr(H,multi_a(1,XPRV))",
 463              "tr($H,multi_a(1,$1/*))",
 464              [True],
 465              lambda k1: (key(H_POINT), [multi_a(1, [k1])])
 466          )
 467          self.do_test(
 468              "tr(H,sortedmulti_a(1,XPRV,XPUB))",
 469              "tr($H,sortedmulti_a(1,$1/*,$2/*))",
 470              [True, False],
 471              lambda k1, k2: (key(H_POINT), [multi_a(1, [k1, k2], True)])
 472          )
 473          self.do_test(
 474              "tr(H,{H,multi_a(1,XPUB,XPRV)})",
 475              "tr($H,{pk($H),multi_a(1,$1/*,$2/*)})",
 476              [False, True],
 477              lambda k1, k2: (key(H_POINT), [pk(H_POINT), [multi_a(1, [k1, k2])]])
 478          )
 479          self.do_test(
 480              "tr(H,sortedmulti_a(1,XPUB,XPRV,XPRV))",
 481              "tr($H,sortedmulti_a(1,$1/*,$2/*,$3/*))",
 482              [False, True, True],
 483              lambda k1, k2, k3: (key(H_POINT), [multi_a(1, [k1, k2, k3], True)])
 484          )
 485          self.do_test(
 486              "tr(H,multi_a(2,XPRV,XPUB,XPRV))",
 487              "tr($H,multi_a(2,$1/*,$2/*,$3/*))",
 488              [True, False, True],
 489              lambda k1, k2, k3: (key(H_POINT), [multi_a(2, [k1, k2, k3])])
 490          )
 491          self.do_test(
 492              "tr(XPUB,{{XPUB,{XPUB,sortedmulti_a(2,XPRV,XPUB,XPRV)}})",
 493              "tr($2/*,{pk($2/*),{pk($2/*),sortedmulti_a(2,$1/*,$2/*,$3/*)}})",
 494              [True, False, True],
 495              lambda k1, k2, k3: (key(k2), [pk(k2), [pk(k2), multi_a(2, [k1, k2, k3], True)]])
 496          )
 497          rnd_pos = random.randrange(MAX_PUBKEYS_PER_MULTI_A)
 498          self.do_test(
 499              "tr(XPUB,multi_a(1,H...,XPRV,H...))",
 500              "tr($2/*,multi_a(1" + (",$H" * rnd_pos) + ",$1/*" + (",$H" * (MAX_PUBKEYS_PER_MULTI_A - 1 - rnd_pos)) + "))",
 501              [True, False],
 502              lambda k1, k2: (key(k2), [multi_a(1, ([H_POINT] * rnd_pos) + [k1] + ([H_POINT] * (MAX_PUBKEYS_PER_MULTI_A - 1 - rnd_pos)))])
 503          )
 504          self.do_test(
 505              "rawtr(XPRV)",
 506              "rawtr($1/*)",
 507              [True],
 508              lambda k1: key(k1)
 509          )
 510  
 511  if __name__ == '__main__':
 512      WalletTaprootTest(__file__).main()
 513