Skip to content

Commit 611e125

Browse files
committed
qa: functional test Miniscript signing with key and timelocks
We'll need a better integration of the hash preimages PSBT fields to satisfy Miniscript with such challenges from the RPC. Thanks to Greg Sanders for his examples and suggestions to improve this test.
1 parent d57b7f2 commit 611e125

File tree

1 file changed

+158
-1
lines changed

1 file changed

+158
-1
lines changed

test/functional/wallet_miniscript.py

+158-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,90 @@
4040
# A Revault Unvault policy with the older() replaced by an after()
4141
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]}/*)))",
4242
# Liquid-like federated pegin with emergency recovery keys
43-
"or_i(and_b(pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),a:and_b(pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),a:and_b(pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),a:and_b(pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),s:pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2))))),and_v(v:thresh(2,pkh(tpubD6NzVbkrYhZ4YK67cd5fDe4fBVmGB2waTDrAt1q4ey9HPq9veHjWkw3VpbaCHCcWozjkhgAkWpFrxuPMUrmXVrLHMfEJ9auoZA6AS1g3grC/*),a:pkh(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),a:pkh(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068)),older(4209713)))",
43+
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)))",
44+
]
45+
46+
MINISCRIPTS_PRIV = [
47+
# One of two keys, of which one private key is known
48+
{
49+
"ms": f"or_i(pk({TPRVS[0]}/*),pk({TPUBS[0]}/*))",
50+
"sequence": None,
51+
"locktime": None,
52+
"sigs_count": 1,
53+
"stack_size": 3,
54+
},
55+
# A more complex policy, that can't be satisfied through the first branch (need for a preimage)
56+
{
57+
"ms": f"andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(2a8ce30189b2ec3200b47aeb4feaac8fcad7c0ba170389729f4898b0b7933bcb)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*)))",
58+
"sequence": 2,
59+
"locktime": None,
60+
"sigs_count": 3,
61+
"stack_size": 5,
62+
},
63+
# Signature with a relative timelock
64+
{
65+
"ms": f"and_v(v:older(2),pk({TPRVS[0]}/*))",
66+
"sequence": 2,
67+
"locktime": None,
68+
"sigs_count": 1,
69+
"stack_size": 2,
70+
},
71+
# Signature with an absolute timelock
72+
{
73+
"ms": f"and_v(v:after(20),pk({TPRVS[0]}/*))",
74+
"sequence": None,
75+
"locktime": 20,
76+
"sigs_count": 1,
77+
"stack_size": 2,
78+
},
79+
# Signature with both
80+
{
81+
"ms": f"and_v(v:older(4),and_v(v:after(30),pk({TPRVS[0]}/*)))",
82+
"sequence": 4,
83+
"locktime": 30,
84+
"sigs_count": 1,
85+
"stack_size": 2,
86+
},
87+
# We have one key on each branch; Core signs both (can't finalize)
88+
{
89+
"ms": f"c:andor(pk({TPRVS[0]}/*),pk_k({TPUBS[0]}),and_v(v:pk({TPRVS[1]}),pk_k({TPUBS[1]})))",
90+
"sequence": None,
91+
"locktime": None,
92+
"sigs_count": 2,
93+
"stack_size": None,
94+
},
95+
# We have all the keys, wallet selects the timeout path to sign since it's smaller and sequence is set
96+
{
97+
"ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pk({TPRVS[1]}),older(10)))",
98+
"sequence": 10,
99+
"locktime": None,
100+
"sigs_count": 3,
101+
"stack_size": 3,
102+
},
103+
# We have all the keys, wallet selects the primary path to sign unconditionally since nsequence wasn't set to be valid for timeout path
104+
{
105+
"ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pkh({TPRVS[1]}),older(10)))",
106+
"sequence": None,
107+
"locktime": None,
108+
"sigs_count": 3,
109+
"stack_size": 3,
110+
},
111+
# Finalizes to the smallest valid witness, regardless of sequence
112+
{
113+
"ms": f"or_d(pk({TPRVS[0]}/*),and_v(v:pk({TPRVS[1]}),and_v(v:pk({TPRVS[2]}),older(10))))",
114+
"sequence": 12,
115+
"locktime": None,
116+
"sigs_count": 3,
117+
"stack_size": 2,
118+
},
119+
# Liquid-like federated pegin with emergency recovery privkeys
120+
{
121+
"ms": f"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)))",
122+
"sequence": 42,
123+
"locktime": None,
124+
"sigs_count": 2,
125+
"stack_size": 8,
126+
},
44127
]
45128

46129

@@ -84,13 +167,77 @@ def watchonly_test(self, ms):
84167
utxo = self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])[0]
85168
assert utxo["txid"] == txid and utxo["solvable"]
86169

170+
def signing_test(self, ms, sequence, locktime, sigs_count, stack_size):
171+
self.log.info(f"Importing private Miniscript '{ms}'")
172+
desc = descsum_create(f"wsh({ms})")
173+
res = self.ms_sig_wallet.importdescriptors(
174+
[
175+
{
176+
"desc": desc,
177+
"active": True,
178+
"range": 0,
179+
"next_index": 0,
180+
"timestamp": "now",
181+
}
182+
]
183+
)
184+
assert res[0]["success"], res
185+
186+
self.log.info("Generating an address for it and testing it detects funds")
187+
addr = self.ms_sig_wallet.getnewaddress()
188+
txid = self.funder.sendtoaddress(addr, 0.01)
189+
self.wait_until(lambda: txid in self.funder.getrawmempool())
190+
self.funder.generatetoaddress(1, self.funder.getnewaddress())
191+
utxo = self.ms_sig_wallet.listunspent(addresses=[addr])[0]
192+
assert txid == utxo["txid"] and utxo["solvable"]
193+
194+
self.log.info("Creating a transaction spending these funds")
195+
dest_addr = self.funder.getnewaddress()
196+
seq = sequence if sequence is not None else 0xFFFFFFFF - 2
197+
lt = locktime if locktime is not None else 0
198+
psbt = self.ms_sig_wallet.createpsbt(
199+
[
200+
{
201+
"txid": txid,
202+
"vout": utxo["vout"],
203+
"sequence": seq,
204+
}
205+
],
206+
[{dest_addr: 0.009}],
207+
lt,
208+
)
209+
210+
self.log.info("Signing it and checking the satisfaction.")
211+
res = self.ms_sig_wallet.walletprocesspsbt(psbt=psbt, finalize=False)
212+
psbtin = self.nodes[0].rpc.decodepsbt(res["psbt"])["inputs"][0]
213+
assert len(psbtin["partial_signatures"]) == sigs_count
214+
res = self.ms_sig_wallet.finalizepsbt(res["psbt"])
215+
assert res["complete"] == (stack_size is not None)
216+
217+
if stack_size is not None:
218+
txin = self.nodes[0].rpc.decoderawtransaction(res["hex"])["vin"][0]
219+
assert len(txin["txinwitness"]) == stack_size, txin["txinwitness"]
220+
self.log.info("Broadcasting the transaction.")
221+
# If necessary, satisfy a relative timelock
222+
if sequence is not None:
223+
self.funder.generatetoaddress(sequence, self.funder.getnewaddress())
224+
# If necessary, satisfy an absolute timelock
225+
height = self.funder.getblockcount()
226+
if locktime is not None and height < locktime:
227+
self.funder.generatetoaddress(
228+
locktime - height, self.funder.getnewaddress()
229+
)
230+
self.ms_sig_wallet.sendrawtransaction(res["hex"])
231+
87232
def run_test(self):
88233
self.log.info("Making a descriptor wallet")
89234
self.funder = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
90235
self.nodes[0].createwallet(
91236
wallet_name="ms_wo", descriptors=True, disable_private_keys=True
92237
)
93238
self.ms_wo_wallet = self.nodes[0].get_wallet_rpc("ms_wo")
239+
self.nodes[0].createwallet(wallet_name="ms_sig", descriptors=True)
240+
self.ms_sig_wallet = self.nodes[0].get_wallet_rpc("ms_sig")
94241

95242
# Sanity check we wouldn't let an insane Miniscript descriptor in
96243
res = self.ms_wo_wallet.importdescriptors(
@@ -111,6 +258,16 @@ def run_test(self):
111258
for ms in MINISCRIPTS:
112259
self.watchonly_test(ms)
113260

261+
# Test we can sign most Miniscript (all but ones requiring preimages, for now)
262+
for ms in MINISCRIPTS_PRIV:
263+
self.signing_test(
264+
ms["ms"],
265+
ms["sequence"],
266+
ms["locktime"],
267+
ms["sigs_count"],
268+
ms["stack_size"],
269+
)
270+
114271

115272
if __name__ == "__main__":
116273
WalletMiniscriptTest().main()

0 commit comments

Comments
 (0)