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