wallet_miniscript.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 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 Miniscript descriptors integration in the wallet."""
   6  
   7  from test_framework.descriptors import descsum_create
   8  from test_framework.psbt import PSBT, PSBT_IN_SHA256
   9  from test_framework.test_framework import LimenkaTestFramework
  10  from test_framework.util import assert_equal
  11  
  12  
  13  TPRVS = [
  14      "tprv8ZgxMBicQKsPerQj6m35no46amfKQdjY7AhLnmatHYXs8S4MTgeZYkWAn4edSGwwL3vkSiiGqSZQrmy5D3P5gBoqgvYP2fCUpBwbKTMTAkL",
  15      "tprv8ZgxMBicQKsPd3cbrKjE5GKKJLDEidhtzSSmPVtSPyoHQGL2LZw49yt9foZsN9BeiC5VqRaESUSDV2PS9w7zAVBSK6EQH3CZW9sMKxSKDwD",
  16      "tprv8iF7W37EHnVEtDr9EFeyFjQJFL6SfGby2AnZ2vQARxTQHQXy9tdzZvBBVp8a19e5vXhskczLkJ1AZjqgScqWL4FpmXVp8LLjiorcrFK63Sr",
  17  ]
  18  TPUBS = [
  19      "tpubD6NzVbkrYhZ4YPAbyf6urxqqnmJF79PzQtyERAmvkSVS9fweCTjxjDh22Z5St9fGb1a5DUCv8G27nYupKP1Ctr1pkamJossoetzws1moNRn",
  20      "tpubD6NzVbkrYhZ4YMQC15JS7QcrsAyfGrGiykweqMmPxTkEVScu7vCZLNpPXW1XphHwzsgmqdHWDQAfucbM72EEB1ZEyfgZxYvkZjYVXx1xS9p",
  21      "tpubD6NzVbkrYhZ4YU9vM1s53UhD75UyJatx8EMzMZ3VUjR2FciNfLLkAw6a4pWACChzobTseNqdWk4G7ZdBqRDLtLSACKykTScmqibb1ZrCvJu",
  22      "tpubD6NzVbkrYhZ4XRMcMFMMFvzVt6jaDAtjZhD7JLwdPdMm9xa76DnxYYP7w9TZGJDVFkek3ArwVsuacheqqPog8TH5iBCX1wuig8PLXim4n9a",
  23      "tpubD6NzVbkrYhZ4WsqRzDmkL82SWcu42JzUvKWzrJHQ8EC2vEHRHkXj1De93sD3biLrKd8XGnamXURGjMbYavbszVDXpjXV2cGUERucLJkE6cy",
  24      "tpubDEFLeBkKTm8aiYkySz8hXAXPVnPSfxMi7Fxhg9sejUrkwJuRWvPdLEiXjTDbhGbjLKCZUDUUibLxTnK5UP1q7qYrSnPqnNe7M8mvAW1STcc",
  25      "tpubD6NzVbkrYhZ4WR99ygpiJvPMAJiwahjLgGywc5vJx2gUfKUfEPCrbKmQczDPJZmLcyZzRb5Ti6rfUb89S2WFyPH7FDtD6RFDA1hdgTEgEUL",
  26  ]
  27  PUBKEYS = [
  28      "02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068",
  29      "030f64b922aee2fd597f104bc6cb3b670f1ca2c6c49b1071a1a6c010575d94fe5a",
  30      "02abe475b199ec3d62fa576faee16a334fdb86ffb26dce75becebaaedf328ac3fe",
  31      "0314f3dc33595b0d016bb522f6fe3a67680723d842c1b9b8ae6b59fdd8ab5cccb4",
  32      "025eba3305bd3c829e4e1551aac7358e4178832c739e4fc4729effe428de0398ab",
  33      "029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0",
  34      "0211c7b2e18b6fd330f322de087da62da92ae2ae3d0b7cec7e616479cce175f183",
  35  ]
  36  
  37  P2WSH_MINISCRIPTS = [
  38      # One of two keys
  39      f"or_b(pk({TPUBS[0]}/*),s:pk({TPUBS[1]}/*))",
  40      # A script similar (same spending policy) to BOLT3's offered HTLC (with anchor outputs)
  41      f"or_d(pk({TPUBS[0]}/*),and_v(and_v(v:pk({TPUBS[1]}/*),or_c(pk({TPUBS[2]}/*),v:hash160(7f999c905d5e35cefd0a37673f746eb13fba3640))),older(1)))",
  42      # A Revault Unvault policy with the older() replaced by an after()
  43      f"andor(multi(2,{TPUBS[0]}/*,{TPUBS[1]}/*),and_v(v:multi(4,{PUBKEYS[0]},{PUBKEYS[1]},{PUBKEYS[2]},{PUBKEYS[3]}),after(424242)),thresh(4,pkh({TPUBS[2]}/*),a:pkh({TPUBS[3]}/*),a:pkh({TPUBS[4]}/*),a:pkh({TPUBS[5]}/*)))",
  44      # Liquid-like federated pegin with emergency recovery keys
  45      f"or_i(and_b(pk({PUBKEYS[0]}),a:and_b(pk({PUBKEYS[1]}),a:and_b(pk({PUBKEYS[2]}),a:and_b(pk({PUBKEYS[3]}),s:pk({PUBKEYS[4]}))))),and_v(v:thresh(2,pkh({TPUBS[0]}/*),a:pkh({PUBKEYS[5]}),a:pkh({PUBKEYS[6]})),older(4209713)))",
  46  ]
  47  
  48  DESCS = [
  49      *[f"wsh({ms})" for ms in P2WSH_MINISCRIPTS],
  50      # A Taproot with one of the above scripts as the single script path.
  51      f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{P2WSH_MINISCRIPTS[0]})",
  52      # A Taproot with two script paths among the above scripts.
  53      f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}})",
  54      # A Taproot with three script paths among the above scripts.
  55      f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}},{P2WSH_MINISCRIPTS[2].replace('multi', 'multi_a')}}})",
  56      # A Taproot with all above scripts in its tree.
  57      f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}},{{{P2WSH_MINISCRIPTS[2].replace('multi', 'multi_a')},{P2WSH_MINISCRIPTS[3]}}}}})",
  58  ]
  59  
  60  DESCS_PRIV = [
  61      # One of two keys, of which one private key is known
  62      {
  63          "desc": f"wsh(or_i(pk({TPRVS[0]}/*),pk({TPUBS[0]}/*)))",
  64          "sequence": None,
  65          "locktime": None,
  66          "sigs_count": 1,
  67          "stack_size": 3,
  68      },
  69      # A more complex policy, that can't be satisfied through the first branch (need for a preimage)
  70      {
  71          "desc": f"wsh(andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(2a8ce30189b2ec3200b47aeb4feaac8fcad7c0ba170389729f4898b0b7933bcb)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*))))",
  72          "sequence": 2,
  73          "locktime": None,
  74          "sigs_count": 3,
  75          "stack_size": 5,
  76      },
  77      # The same policy but we provide the preimage. This path will be chosen as it's a smaller witness.
  78      {
  79          "desc": f"wsh(andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(61e33e9dbfefc45f6a194187684d278f789fd4d5e207a357e79971b6519a8b12)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*))))",
  80          "sequence": 2,
  81          "locktime": None,
  82          "sigs_count": 3,
  83          "stack_size": 4,
  84          "sha256_preimages": {
  85              "61e33e9dbfefc45f6a194187684d278f789fd4d5e207a357e79971b6519a8b12": "e8774f330f5f330c23e8bbefc5595cb87009ddb7ac3b8deaaa8e9e41702d919c"
  86          },
  87      },
  88      # Signature with a relative timelock
  89      {
  90          "desc": f"wsh(and_v(v:older(2),pk({TPRVS[0]}/*)))",
  91          "sequence": 2,
  92          "locktime": None,
  93          "sigs_count": 1,
  94          "stack_size": 2,
  95      },
  96      # Signature with an absolute timelock
  97      {
  98          "desc": f"wsh(and_v(v:after(20),pk({TPRVS[0]}/*)))",
  99          "sequence": None,
 100          "locktime": 20,
 101          "sigs_count": 1,
 102          "stack_size": 2,
 103      },
 104      # Signature with both
 105      {
 106          "desc": f"wsh(and_v(v:older(4),and_v(v:after(30),pk({TPRVS[0]}/*))))",
 107          "sequence": 4,
 108          "locktime": 30,
 109          "sigs_count": 1,
 110          "stack_size": 2,
 111      },
 112      # We have one key on each branch; Core signs both (can't finalize)
 113      {
 114          "desc": f"wsh(c:andor(pk({TPRVS[0]}/*),pk_k({TPUBS[0]}),and_v(v:pk({TPRVS[1]}),pk_k({TPUBS[1]}))))",
 115          "sequence": None,
 116          "locktime": None,
 117          "sigs_count": 2,
 118          "stack_size": None,
 119      },
 120      # We have all the keys, wallet selects the timeout path to sign since it's smaller and sequence is set
 121      {
 122          "desc": f"wsh(andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pk({TPRVS[1]}),older(10))))",
 123          "sequence": 10,
 124          "locktime": None,
 125          "sigs_count": 3,
 126          "stack_size": 3,
 127      },
 128      # We have all the keys, wallet selects the primary path to sign unconditionally since nsequence wasn't set to be valid for timeout path
 129      {
 130          "desc": f"wsh(andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pkh({TPRVS[1]}),older(10))))",
 131          "sequence": None,
 132          "locktime": None,
 133          "sigs_count": 3,
 134          "stack_size": 3,
 135      },
 136      # Finalizes to the smallest valid witness, regardless of sequence
 137      {
 138          "desc": f"wsh(or_d(pk({TPRVS[0]}/*),and_v(v:pk({TPRVS[1]}),and_v(v:pk({TPRVS[2]}),older(10)))))",
 139          "sequence": 12,
 140          "locktime": None,
 141          "sigs_count": 3,
 142          "stack_size": 2,
 143      },
 144      # Liquid-like federated pegin with emergency recovery privkeys
 145      {
 146          "desc": f"wsh(or_i(and_b(pk({TPUBS[0]}/*),a:and_b(pk({TPUBS[1]}),a:and_b(pk({TPUBS[2]}),a:and_b(pk({TPUBS[3]}),s:pk({PUBKEYS[0]}))))),and_v(v:thresh(2,pkh({TPRVS[0]}),a:pkh({TPRVS[1]}),a:pkh({TPUBS[4]})),older(42))))",
 147          "sequence": 42,
 148          "locktime": None,
 149          "sigs_count": 2,
 150          "stack_size": 8,
 151      },
 152      # Each leaf needs two sigs. We've got one key on each. Will sign both but can't finalize.
 153      {
 154          "desc": f"tr({TPUBS[0]}/*,{{and_v(v:pk({TPRVS[0]}/*),pk({TPUBS[1]})),and_v(v:pk({TPRVS[1]}/*),pk({TPUBS[2]}))}})",
 155          "sequence": None,
 156          "locktime": None,
 157          "sigs_count": 2,
 158          "stack_size": None,
 159      },
 160      # The same but now the two leaves are identical. Will add a single sig that is valid for both. Can't finalize.
 161      {
 162          "desc": f"tr({TPUBS[0]}/*,{{and_v(v:pk({TPRVS[0]}/*),pk({TPUBS[1]})),and_v(v:pk({TPRVS[0]}/*),pk({TPUBS[1]}))}})",
 163          "sequence": None,
 164          "locktime": None,
 165          "sigs_count": 1,
 166          "stack_size": None,
 167      },
 168      # The same but we have the two necessary privkeys on one of the leaves. Also it uses a pubkey hash.
 169      {
 170          "desc": f"tr({TPUBS[0]}/*,{{and_v(v:pk({TPRVS[0]}/*),pk({TPUBS[1]})),and_v(v:pkh({TPRVS[1]}/*),pk({TPRVS[2]}))}})",
 171          "sequence": None,
 172          "locktime": None,
 173          "sigs_count": 3,
 174          "stack_size": 5,
 175      },
 176      # A key immediately or one of two keys after a timelock. If both paths are available it'll use the
 177      # non-timelocked path because it's a smaller witness.
 178      {
 179          "desc": f"tr({TPUBS[0]}/*,{{pk({TPRVS[0]}/*),and_v(v:older(42),multi_a(1,{TPRVS[1]},{TPRVS[2]}))}})",
 180          "sequence": 42,
 181          "locktime": None,
 182          "sigs_count": 3,
 183          "stack_size": 3,
 184      },
 185      # A key immediately or one of two keys after a timelock. If the "primary" key isn't available though it'll
 186      # use the timelocked path. Same remark for multi_a.
 187      {
 188          "desc": f"tr({TPUBS[0]}/*,{{pk({TPUBS[1]}/*),and_v(v:older(42),multi_a(1,{TPRVS[0]},{TPRVS[1]}))}})",
 189          "sequence": 42,
 190          "locktime": None,
 191          "sigs_count": 2,
 192          "stack_size": 4,
 193      },
 194      # Liquid-like federated pegin with emergency recovery privkeys, but in a Taproot.
 195      {
 196          "desc": f"tr({TPUBS[1]}/*,{{and_b(pk({TPUBS[2]}/*),a:and_b(pk({TPUBS[3]}),a:and_b(pk({TPUBS[4]}),a:and_b(pk({TPUBS[5]}),s:pk({PUBKEYS[0]}))))),and_v(v:thresh(2,pkh({TPRVS[0]}),a:pkh({TPRVS[1]}),a:pkh({TPUBS[6]})),older(42))}})",
 197          "sequence": 42,
 198          "locktime": None,
 199          "sigs_count": 2,
 200          "stack_size": 8,
 201      },
 202  ]
 203  
 204  
 205  class WalletMiniscriptTest(LimenkaTestFramework):
 206      def add_options(self, parser):
 207          self.add_wallet_options(parser, legacy=False)
 208  
 209      def set_test_params(self):
 210          self.num_nodes = 1
 211          self.rpc_timeout = 180
 212  
 213      def skip_test_if_missing_module(self):
 214          self.skip_if_no_wallet()
 215          self.skip_if_no_sqlite()
 216  
 217      def watchonly_test(self, desc):
 218          self.log.info(f"Importing descriptor '{desc}'")
 219          desc = descsum_create(f"{desc}")
 220          assert self.ms_wo_wallet.importdescriptors(
 221              [
 222                  {
 223                      "desc": desc,
 224                      "active": True,
 225                      "range": 2,
 226                      "next_index": 0,
 227                      "timestamp": "now",
 228                  }
 229              ]
 230          )[0]["success"]
 231  
 232          self.log.info("Testing we derive new addresses for it")
 233          addr_type = "bech32m" if desc.startswith("tr(") else "bech32"
 234          assert_equal(
 235              self.ms_wo_wallet.getnewaddress(address_type=addr_type),
 236              self.funder.deriveaddresses(desc, 0)[0],
 237          )
 238          assert_equal(
 239              self.ms_wo_wallet.getnewaddress(address_type=addr_type),
 240              self.funder.deriveaddresses(desc, 1)[1],
 241          )
 242  
 243          self.log.info("Testing we detect funds sent to one of them")
 244          addr = self.ms_wo_wallet.getnewaddress()
 245          txid = self.funder.sendtoaddress(addr, 0.01)
 246          self.wait_until(
 247              lambda: len(self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])) == 1
 248          )
 249          utxo = self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])[0]
 250          assert utxo["txid"] == txid and utxo["solvable"]
 251  
 252      def signing_test(
 253          self, desc, sequence, locktime, sigs_count, stack_size, sha256_preimages
 254      ):
 255          self.log.info(f"Importing private Miniscript descriptor '{desc}'")
 256          is_taproot = desc.startswith("tr(")
 257          desc = descsum_create(desc)
 258          res = self.ms_sig_wallet.importdescriptors(
 259              [
 260                  {
 261                      "desc": desc,
 262                      "active": True,
 263                      "range": 0,
 264                      "next_index": 0,
 265                      "timestamp": "now",
 266                  }
 267              ]
 268          )
 269          assert res[0]["success"], res
 270  
 271          self.log.info("Generating an address for it and testing it detects funds")
 272          addr_type = "bech32m" if is_taproot else "bech32"
 273          addr = self.ms_sig_wallet.getnewaddress(address_type=addr_type)
 274          txid = self.funder.sendtoaddress(addr, 0.01)
 275          self.wait_until(lambda: txid in self.funder.getrawmempool())
 276          self.funder.generatetoaddress(1, self.funder.getnewaddress())
 277          utxo = self.ms_sig_wallet.listunspent(addresses=[addr])[0]
 278          assert txid == utxo["txid"] and utxo["solvable"]
 279  
 280          self.log.info("Creating a transaction spending these funds")
 281          dest_addr = self.funder.getnewaddress()
 282          seq = sequence if sequence is not None else 0xFFFFFFFF - 2
 283          lt = locktime if locktime is not None else 0
 284          psbt = self.ms_sig_wallet.createpsbt(
 285              [
 286                  {
 287                      "txid": txid,
 288                      "vout": utxo["vout"],
 289                      "sequence": seq,
 290                  }
 291              ],
 292              [{dest_addr: 0.009}],
 293              lt,
 294          )
 295  
 296          self.log.info("Signing it and checking the satisfaction.")
 297          if sha256_preimages is not None:
 298              psbt = PSBT.from_base64(psbt)
 299              for (h, preimage) in sha256_preimages.items():
 300                  k = PSBT_IN_SHA256.to_bytes(1, "big") + bytes.fromhex(h)
 301                  psbt.i[0].map[k] = bytes.fromhex(preimage)
 302              psbt = psbt.to_base64()
 303          res = self.ms_sig_wallet.walletprocesspsbt(psbt=psbt, finalize=False)
 304          psbtin = self.nodes[0].rpc.decodepsbt(res["psbt"])["inputs"][0]
 305          sigs_field_name = "taproot_script_path_sigs" if is_taproot else "partial_signatures"
 306          assert len(psbtin[sigs_field_name]) == sigs_count
 307          res = self.ms_sig_wallet.finalizepsbt(res["psbt"])
 308          assert res["complete"] == (stack_size is not None)
 309  
 310          if stack_size is not None:
 311              txin = self.nodes[0].rpc.decoderawtransaction(res["hex"])["vin"][0]
 312              assert len(txin["txinwitness"]) == stack_size, txin["txinwitness"]
 313              self.log.info("Broadcasting the transaction.")
 314              # If necessary, satisfy a relative timelock
 315              if sequence is not None:
 316                  self.funder.generatetoaddress(sequence, self.funder.getnewaddress())
 317              # If necessary, satisfy an absolute timelock
 318              height = self.funder.getblockcount()
 319              if locktime is not None and height < locktime:
 320                  self.funder.generatetoaddress(
 321                      locktime - height, self.funder.getnewaddress()
 322                  )
 323              self.ms_sig_wallet.sendrawtransaction(res["hex"])
 324  
 325      def run_test(self):
 326          self.log.info("Making a descriptor wallet")
 327          self.funder = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
 328          self.nodes[0].createwallet(
 329              wallet_name="ms_wo", descriptors=True, disable_private_keys=True
 330          )
 331          self.ms_wo_wallet = self.nodes[0].get_wallet_rpc("ms_wo")
 332          self.nodes[0].createwallet(wallet_name="ms_sig", descriptors=True)
 333          self.ms_sig_wallet = self.nodes[0].get_wallet_rpc("ms_sig")
 334  
 335          # Sanity check we wouldn't let an insane Miniscript descriptor in
 336          res = self.ms_wo_wallet.importdescriptors(
 337              [
 338                  {
 339                      "desc": descsum_create(
 340                          "wsh(and_b(ripemd160(1fd9b55a054a2b3f658d97e6b84cf3ee00be429a),a:1))"
 341                      ),
 342                      "active": False,
 343                      "timestamp": "now",
 344                  }
 345              ]
 346          )[0]
 347          assert not res["success"]
 348          assert "is not sane: witnesses without signature exist" in res["error"]["message"]
 349  
 350          # Sanity check we wouldn't let an unspendable Miniscript descriptor in
 351          res = self.ms_wo_wallet.importdescriptors(
 352              [
 353                  {
 354                      "desc": descsum_create("wsh(0)"),
 355                      "active": False,
 356                      "timestamp": "now",
 357                  }
 358              ]
 359          )[0]
 360          assert not res["success"] and "is not satisfiable" in res["error"]["message"]
 361  
 362          # Test we can track any type of Miniscript
 363          for desc in DESCS:
 364              self.watchonly_test(desc)
 365  
 366          # Test we can sign for any Miniscript.
 367          for desc in DESCS_PRIV:
 368              self.signing_test(
 369                  desc["desc"],
 370                  desc["sequence"],
 371                  desc["locktime"],
 372                  desc["sigs_count"],
 373                  desc["stack_size"],
 374                  desc.get("sha256_preimages"),
 375              )
 376  
 377          # Test we can sign for a max-size TapMiniscript. Recompute the maximum accepted size
 378          # for a TapMiniscript (see cpp file for details). Then pad a simple pubkey check up
 379          # to the maximum size. Make sure we can import and spend this script.
 380          leeway_weight = (4 + 4 + 1 + 36 + 4 + 1 + 1 + 8 + 1 + 1 + 33) * 4 + 2
 381          max_tapmini_size = 400_000 - 3 - (1 + 65) * 1_000 - 3 - (33 + 32 * 128) - leeway_weight - 5
 382          padding = max_tapmini_size - 33 - 1
 383          ms = f"pk({TPRVS[0]}/*)"
 384          ms = "n" * padding + ":" + ms
 385          desc = f"tr({PUBKEYS[0]},{ms})"
 386          self.signing_test(desc, None, None, 1, 3, None)
 387          # This was really the maximum size, one more byte and we can't import it.
 388          ms = "n" + ms
 389          desc = f"tr({PUBKEYS[0]},{ms})"
 390          res = self.ms_wo_wallet.importdescriptors(
 391              [
 392                  {
 393                      "desc": descsum_create(desc),
 394                      "active": False,
 395                      "timestamp": "now",
 396                  }
 397              ]
 398          )[0]
 399          assert not res["success"]
 400          assert "is not a valid descriptor function" in res["error"]["message"]
 401  
 402  
 403  if __name__ == "__main__":
 404      WalletMiniscriptTest(__file__).main()
 405