From 611e12502a5887ffb751bb92fadaa334d484824b Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 20 Jan 2022 16:36:16 +0100 Subject: 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. --- test/functional/wallet_miniscript.py | 159 ++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index 4b860118dc..e8b89513d0 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -40,7 +40,90 @@ MINISCRIPTS = [ # A Revault Unvault policy with the older() replaced by an after() 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]}/*)))", # Liquid-like federated pegin with emergency recovery keys - "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)))", + 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)))", +] + +MINISCRIPTS_PRIV = [ + # One of two keys, of which one private key is known + { + "ms": f"or_i(pk({TPRVS[0]}/*),pk({TPUBS[0]}/*))", + "sequence": None, + "locktime": None, + "sigs_count": 1, + "stack_size": 3, + }, + # A more complex policy, that can't be satisfied through the first branch (need for a preimage) + { + "ms": f"andor(ndv:older(2),and_v(v:pk({TPRVS[0]}),sha256(2a8ce30189b2ec3200b47aeb4feaac8fcad7c0ba170389729f4898b0b7933bcb)),and_v(v:pkh({TPRVS[1]}),pk({TPRVS[2]}/*)))", + "sequence": 2, + "locktime": None, + "sigs_count": 3, + "stack_size": 5, + }, + # Signature with a relative timelock + { + "ms": f"and_v(v:older(2),pk({TPRVS[0]}/*))", + "sequence": 2, + "locktime": None, + "sigs_count": 1, + "stack_size": 2, + }, + # Signature with an absolute timelock + { + "ms": f"and_v(v:after(20),pk({TPRVS[0]}/*))", + "sequence": None, + "locktime": 20, + "sigs_count": 1, + "stack_size": 2, + }, + # Signature with both + { + "ms": f"and_v(v:older(4),and_v(v:after(30),pk({TPRVS[0]}/*)))", + "sequence": 4, + "locktime": 30, + "sigs_count": 1, + "stack_size": 2, + }, + # We have one key on each branch; Core signs both (can't finalize) + { + "ms": f"c:andor(pk({TPRVS[0]}/*),pk_k({TPUBS[0]}),and_v(v:pk({TPRVS[1]}),pk_k({TPUBS[1]})))", + "sequence": None, + "locktime": None, + "sigs_count": 2, + "stack_size": None, + }, + # We have all the keys, wallet selects the timeout path to sign since it's smaller and sequence is set + { + "ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pk({TPRVS[1]}),older(10)))", + "sequence": 10, + "locktime": None, + "sigs_count": 3, + "stack_size": 3, + }, + # We have all the keys, wallet selects the primary path to sign unconditionally since nsequence wasn't set to be valid for timeout path + { + "ms": f"andor(pk({TPRVS[0]}/*),pk({TPRVS[2]}),and_v(v:pkh({TPRVS[1]}),older(10)))", + "sequence": None, + "locktime": None, + "sigs_count": 3, + "stack_size": 3, + }, + # Finalizes to the smallest valid witness, regardless of sequence + { + "ms": f"or_d(pk({TPRVS[0]}/*),and_v(v:pk({TPRVS[1]}),and_v(v:pk({TPRVS[2]}),older(10))))", + "sequence": 12, + "locktime": None, + "sigs_count": 3, + "stack_size": 2, + }, + # Liquid-like federated pegin with emergency recovery privkeys + { + "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)))", + "sequence": 42, + "locktime": None, + "sigs_count": 2, + "stack_size": 8, + }, ] @@ -84,6 +167,68 @@ class WalletMiniscriptTest(BitcoinTestFramework): utxo = self.ms_wo_wallet.listunspent(minconf=0, addresses=[addr])[0] assert utxo["txid"] == txid and utxo["solvable"] + def signing_test(self, ms, sequence, locktime, sigs_count, stack_size): + self.log.info(f"Importing private Miniscript '{ms}'") + desc = descsum_create(f"wsh({ms})") + res = self.ms_sig_wallet.importdescriptors( + [ + { + "desc": desc, + "active": True, + "range": 0, + "next_index": 0, + "timestamp": "now", + } + ] + ) + assert res[0]["success"], res + + self.log.info("Generating an address for it and testing it detects funds") + addr = self.ms_sig_wallet.getnewaddress() + txid = self.funder.sendtoaddress(addr, 0.01) + self.wait_until(lambda: txid in self.funder.getrawmempool()) + self.funder.generatetoaddress(1, self.funder.getnewaddress()) + utxo = self.ms_sig_wallet.listunspent(addresses=[addr])[0] + assert txid == utxo["txid"] and utxo["solvable"] + + self.log.info("Creating a transaction spending these funds") + dest_addr = self.funder.getnewaddress() + seq = sequence if sequence is not None else 0xFFFFFFFF - 2 + lt = locktime if locktime is not None else 0 + psbt = self.ms_sig_wallet.createpsbt( + [ + { + "txid": txid, + "vout": utxo["vout"], + "sequence": seq, + } + ], + [{dest_addr: 0.009}], + lt, + ) + + self.log.info("Signing it and checking the satisfaction.") + res = self.ms_sig_wallet.walletprocesspsbt(psbt=psbt, finalize=False) + psbtin = self.nodes[0].rpc.decodepsbt(res["psbt"])["inputs"][0] + assert len(psbtin["partial_signatures"]) == sigs_count + res = self.ms_sig_wallet.finalizepsbt(res["psbt"]) + assert res["complete"] == (stack_size is not None) + + if stack_size is not None: + txin = self.nodes[0].rpc.decoderawtransaction(res["hex"])["vin"][0] + assert len(txin["txinwitness"]) == stack_size, txin["txinwitness"] + self.log.info("Broadcasting the transaction.") + # If necessary, satisfy a relative timelock + if sequence is not None: + self.funder.generatetoaddress(sequence, self.funder.getnewaddress()) + # If necessary, satisfy an absolute timelock + height = self.funder.getblockcount() + if locktime is not None and height < locktime: + self.funder.generatetoaddress( + locktime - height, self.funder.getnewaddress() + ) + self.ms_sig_wallet.sendrawtransaction(res["hex"]) + def run_test(self): self.log.info("Making a descriptor wallet") self.funder = self.nodes[0].get_wallet_rpc(self.default_wallet_name) @@ -91,6 +236,8 @@ class WalletMiniscriptTest(BitcoinTestFramework): wallet_name="ms_wo", descriptors=True, disable_private_keys=True ) self.ms_wo_wallet = self.nodes[0].get_wallet_rpc("ms_wo") + self.nodes[0].createwallet(wallet_name="ms_sig", descriptors=True) + self.ms_sig_wallet = self.nodes[0].get_wallet_rpc("ms_sig") # Sanity check we wouldn't let an insane Miniscript descriptor in res = self.ms_wo_wallet.importdescriptors( @@ -111,6 +258,16 @@ class WalletMiniscriptTest(BitcoinTestFramework): for ms in MINISCRIPTS: self.watchonly_test(ms) + # Test we can sign most Miniscript (all but ones requiring preimages, for now) + for ms in MINISCRIPTS_PRIV: + self.signing_test( + ms["ms"], + ms["sequence"], + ms["locktime"], + ms["sigs_count"], + ms["stack_size"], + ) + if __name__ == "__main__": WalletMiniscriptTest().main() -- cgit v1.2.3