wallet_miniscript.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2022-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 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 BitcoinTestFramework 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(BitcoinTestFramework): 206 def set_test_params(self): 207 self.num_nodes = 1 208 self.rpc_timeout = 180 209 210 def skip_test_if_missing_module(self): 211 self.skip_if_no_wallet() 212 213 def watchonly_test(self, desc): 214 self.log.info(f"Importing descriptor '{desc}'") 215 desc = descsum_create(f"{desc}") 216 assert self.ms_wo_wallet.importdescriptors( 217 [ 218 { 219 "desc": desc, 220 "active": True, 221 "range": 2, 222 "next_index": 0, 223 "timestamp": "now", 224 } 225 ] 226 )[0]["success"] 227 228 self.log.info("Testing we derive new addresses for it") 229 addr_type = "bech32m" if desc.startswith("tr(") else "bech32" 230 assert_equal( 231 self.ms_wo_wallet.getnewaddress(address_type=addr_type), 232 self.funder.deriveaddresses(desc, 0)[0], 233 ) 234 assert_equal( 235 self.ms_wo_wallet.getnewaddress(address_type=addr_type), 236 self.funder.deriveaddresses(desc, 1)[1], 237 ) 238 239 self.log.info("Testing we detect funds sent to one of them") 240 addr = self.ms_wo_wallet.getnewaddress() 241 txid = self.funder.sendtoaddress(addr, 0.01) 242 self.wait_until( 243 lambda: len(self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])) == 1 244 ) 245 utxo = self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])[0] 246 assert utxo["txid"] == txid and utxo["solvable"] 247 248 def signing_test( 249 self, desc, sequence, locktime, sigs_count, stack_size, sha256_preimages 250 ): 251 self.log.info(f"Importing private Miniscript descriptor '{desc}'") 252 is_taproot = desc.startswith("tr(") 253 desc = descsum_create(desc) 254 res = self.ms_sig_wallet.importdescriptors( 255 [ 256 { 257 "desc": desc, 258 "active": True, 259 "range": 0, 260 "next_index": 0, 261 "timestamp": "now", 262 } 263 ] 264 ) 265 assert res[0]["success"], res 266 267 self.log.info("Generating an address for it and testing it detects funds") 268 addr_type = "bech32m" if is_taproot else "bech32" 269 addr = self.ms_sig_wallet.getnewaddress(address_type=addr_type) 270 txid = self.funder.sendtoaddress(addr, 0.01) 271 self.wait_until(lambda: txid in self.funder.getrawmempool()) 272 self.funder.generatetoaddress(1, self.funder.getnewaddress()) 273 utxo = self.ms_sig_wallet.listunspent(addresses=[addr])[0] 274 assert txid == utxo["txid"] and utxo["solvable"] 275 276 self.log.info("Creating a transaction spending these funds") 277 dest_addr = self.funder.getnewaddress() 278 seq = sequence if sequence is not None else 0xFFFFFFFF - 2 279 lt = locktime if locktime is not None else 0 280 psbt = self.ms_sig_wallet.createpsbt( 281 [ 282 { 283 "txid": txid, 284 "vout": utxo["vout"], 285 "sequence": seq, 286 } 287 ], 288 [{dest_addr: 0.009}], 289 lt, 290 ) 291 292 self.log.info("Signing it and checking the satisfaction.") 293 if sha256_preimages is not None: 294 psbt = PSBT.from_base64(psbt) 295 for (h, preimage) in sha256_preimages.items(): 296 k = PSBT_IN_SHA256.to_bytes(1, "big") + bytes.fromhex(h) 297 psbt.i[0].map[k] = bytes.fromhex(preimage) 298 psbt = psbt.to_base64() 299 res = self.ms_sig_wallet.walletprocesspsbt(psbt=psbt, finalize=False) 300 psbtin = self.nodes[0].decodepsbt(res["psbt"])["inputs"][0] 301 sigs_field_name = "taproot_script_path_sigs" if is_taproot else "partial_signatures" 302 assert len(psbtin[sigs_field_name]) == sigs_count 303 res = self.ms_sig_wallet.finalizepsbt(res["psbt"]) 304 assert res["complete"] == (stack_size is not None) 305 306 if stack_size is not None: 307 txin = self.nodes[0].decoderawtransaction(res["hex"])["vin"][0] 308 assert len(txin["txinwitness"]) == stack_size, txin["txinwitness"] 309 self.log.info("Broadcasting the transaction.") 310 # If necessary, satisfy a relative timelock 311 if sequence is not None: 312 self.funder.generatetoaddress(sequence, self.funder.getnewaddress()) 313 # If necessary, satisfy an absolute timelock 314 height = self.funder.getblockcount() 315 if locktime is not None and height < locktime: 316 self.funder.generatetoaddress( 317 locktime - height, self.funder.getnewaddress() 318 ) 319 self.ms_sig_wallet.sendrawtransaction(res["hex"]) 320 321 def run_test(self): 322 self.log.info("Making a descriptor wallet") 323 self.funder = self.nodes[0].get_wallet_rpc(self.default_wallet_name) 324 self.nodes[0].createwallet( 325 wallet_name="ms_wo", disable_private_keys=True 326 ) 327 self.ms_wo_wallet = self.nodes[0].get_wallet_rpc("ms_wo") 328 self.nodes[0].createwallet(wallet_name="ms_sig") 329 self.ms_sig_wallet = self.nodes[0].get_wallet_rpc("ms_sig") 330 331 # Sanity check we wouldn't let an insane Miniscript descriptor in 332 res = self.ms_wo_wallet.importdescriptors( 333 [ 334 { 335 "desc": descsum_create( 336 "wsh(and_b(ripemd160(1fd9b55a054a2b3f658d97e6b84cf3ee00be429a),a:1))" 337 ), 338 "active": False, 339 "timestamp": "now", 340 } 341 ] 342 )[0] 343 assert not res["success"] 344 assert "is not sane: witnesses without signature exist" in res["error"]["message"] 345 346 # Sanity check we wouldn't let an unspendable Miniscript descriptor in 347 res = self.ms_wo_wallet.importdescriptors( 348 [ 349 { 350 "desc": descsum_create("wsh(0)"), 351 "active": False, 352 "timestamp": "now", 353 } 354 ] 355 )[0] 356 assert not res["success"] and "is not satisfiable" in res["error"]["message"] 357 358 # Test we can track any type of Miniscript 359 for desc in DESCS: 360 self.watchonly_test(desc) 361 362 # Test we can sign for any Miniscript. 363 for desc in DESCS_PRIV: 364 self.signing_test( 365 desc["desc"], 366 desc["sequence"], 367 desc["locktime"], 368 desc["sigs_count"], 369 desc["stack_size"], 370 desc.get("sha256_preimages"), 371 ) 372 373 # Test we can sign for a max-size TapMiniscript. Recompute the maximum accepted size 374 # for a TapMiniscript (see cpp file for details). Then pad a simple pubkey check up 375 # to the maximum size. Make sure we can import and spend this script. 376 leeway_weight = (4 + 4 + 1 + 36 + 4 + 1 + 1 + 8 + 1 + 1 + 33) * 4 + 2 377 max_tapmini_size = 400_000 - 3 - (1 + 65) * 1_000 - 3 - (33 + 32 * 128) - leeway_weight - 5 378 padding = max_tapmini_size - 33 - 1 379 ms = f"pk({TPRVS[0]}/*)" 380 ms = "n" * padding + ":" + ms 381 desc = f"tr({PUBKEYS[0]},{ms})" 382 self.signing_test(desc, None, None, 1, 3, None) 383 # This was really the maximum size, one more byte and we can't import it. 384 ms = "n" + ms 385 desc = f"tr({PUBKEYS[0]},{ms})" 386 res = self.ms_wo_wallet.importdescriptors( 387 [ 388 { 389 "desc": descsum_create(desc), 390 "active": False, 391 "timestamp": "now", 392 } 393 ] 394 )[0] 395 assert not res["success"] 396 assert "is not a valid descriptor function" in res["error"]["message"] 397 398 399 if __name__ == "__main__": 400 WalletMiniscriptTest(__file__).main()