mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-05 14:06:27 -05:00
Merge bitcoin/bitcoin#29156: tests: add functional test for miniscript decaying multisig
bb633c9407
tests: add functional test for miniscript decaying multisig (Michael Dietz) Pull request description: This is very closely based on [test/functional/wallet_multisig_descriptor_psbt.py](https://github.com/bitcoin/bitcoin/blob/master/test/functional/wallet_multisig_descriptor_psbt.py) both in code and concept. It should serve as some integration testing for Miniscript descriptors, and also documents a simple multisig that starts as 4-of-4 and decays to 3-of-4, 2-of-4, and finally 1-of-4 at block heights (I think in the real world aligning this to halvenings would be nice). ACKs for top commit: achow101: ACKbb633c9407
rkrux: reACKbb633c9407
hodlinator: ACKbb633c9407
Tree-SHA512: 1f8e8e50258d45d8f2b882b5f86dcd390d86c543ff4801f397733017102e0854ac387960b6e296bb164603545615d224a4b400247cbbc07bf21b2f4b718ab2ff
This commit is contained in:
commit
06757af2da
3 changed files with 149 additions and 0 deletions
|
@ -27,6 +27,8 @@ Supporting RPCs are:
|
|||
by `scanblocks`) and returns rich event data related to spends or receives associated
|
||||
with the given descriptors.
|
||||
|
||||
Bitcoin Core v24 extended `wsh()` output descriptor with [Miniscript](https://bitcoin.sipa.be/miniscript/) support (initially watch-only). Signing support for Miniscript descriptors was added in v25. And since v26 Miniscript expressions can now be used in Taproot descriptors.
|
||||
|
||||
This document describes the language. For the specifics on usage, see the RPC
|
||||
documentation for the functions mentioned above.
|
||||
|
||||
|
@ -45,6 +47,7 @@ Output descriptors currently support:
|
|||
- Any type of supported address through the `addr` function.
|
||||
- Raw hex scripts through the `raw` function.
|
||||
- Public keys (compressed and uncompressed) in hex notation, or BIP32 extended pubkeys with derivation paths.
|
||||
- [Miniscript](https://bitcoin.sipa.be/miniscript/) expressions in `wsh` (P2WSH) and `tr` (P2TR) functions.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -67,6 +70,7 @@ Output descriptors currently support:
|
|||
- `tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,{pk(fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),pk(e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)})` describes a P2TR output with the `c6...` x-only pubkey as internal key, and two script paths.
|
||||
- `tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,sortedmulti_a(2,2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc))` describes a P2TR output with the `c6...` x-only pubkey as internal key, and a single `multi_a` script that needs 2 signatures with 2 specified x-only keys, which will be sorted lexicographically.
|
||||
- `wsh(sortedmulti(2,[6f53d49c/44h/1h/0h]tpubDDjsCRDQ9YzyaAq9rspCfq8RZFrWoBpYnLxK6sS2hS2yukqSczgcYiur8Scx4Hd5AZatxTuzMtJQJhchufv1FRFanLqUP7JHwusSSpfcEp2/0/*,[e6807791/44h/1h/0h]tpubDDAfvogaaAxaFJ6c15ht7Tq6ZmiqFYfrSmZsHu7tHXBgnjMZSHAeHSwhvjARNA6Qybon4ksPksjRbPDVp7yXA1KjTjSd5x18KHqbppnXP1s/0/*,[367c9cfa/44h/1h/0h]tpubDDtPnSgWYk8dDnaDwnof4ehcnjuL5VoUt1eW2MoAed1grPHuXPDnkX1fWMvXfcz3NqFxPbhqNZ3QBdYjLz2hABeM9Z2oqMR1Gt2HHYDoCgh/0/*))#av0kxgw0` describes a *2-of-3* multisig. For brevity, the internal "change" descriptor accompanying the above external "receiving" descriptor is not included here, but it typically differs only in the xpub derivation steps, ending in `/1/*` for change addresses.
|
||||
- `wsh(thresh(4,pk([7258e4f9/44h/1h/0h]tpubDCZrkQoEU3845aFKUu9VQBYWZtrTwxMzcxnBwKFCYXHD6gEXvtFcxddCCLFsEwmxQaG15izcHxj48SXg1QS5FQGMBx5Ak6deXKPAL7wauBU/0/*),s:pk([c80b1469/44h/1h/0h]tpubDD3UwwHoNUF4F3Vi5PiUVTc3ji1uThuRfFyBexTSHoAcHuWW2z8qEE2YujegcLtgthr3wMp3ZauvNG9eT9xfJyxXCfNty8h6rDBYU8UU1qq/0/*),s:pk([4e5024fe/44h/1h/0h]tpubDDLrpPymPLSCJyCMLQdmcWxrAWwsqqssm5NdxT2WSdEBPSXNXxwbeKtsHAyXPpLkhUyKovtZgCi47QxVpw9iVkg95UUgeevyAqtJ9dqBqa1/0/*),s:pk([3b1d1ee9/44h/1h/0h]tpubDCmDTANBWPzf6d8Ap1J5Ku7J1Ay92MpHMrEV7M5muWxCrTBN1g5f1NPcjMEL6dJHxbvEKNZtYCdowaSTN81DAyLsmv6w6xjJHCQNkxrsrfu/0/*),sln:after(840000),sln:after(1050000),sln:after(1260000)))#k28080kv` describes a Miniscript multisig with spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))` that starts as 4-of-4 and "decays" to 3-of-4, 2-of-4, and finally 1-of-4 at each future halvening block height. For brevity, the internal "change" descriptor accompanying the above external "receiving" descriptor is not included here, but it typically differs only in the xpub derivation steps, ending in `/1/*` for change addresses.
|
||||
|
||||
## Reference
|
||||
|
||||
|
@ -195,6 +199,14 @@ preferable in cases where there are more signers. This signing flow is also incl
|
|||
[The test](/test/functional/wallet_multisig_descriptor_psbt.py) is meant to be documentation as much as it is a functional test, so
|
||||
it is kept as simple and readable as possible.
|
||||
|
||||
#### Basic Miniscript-enabled "decaying" multisig example
|
||||
|
||||
For an example of a multisig that starts as 4-of-4 and "decays" to 3-of-4, 2-of-4, and finally 1-of-4 at each future halvening block height, see [this functional test](/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py).
|
||||
|
||||
This has the same "architecture" and signing flow as the above [Basic multisig example](#basic-multisig-example). The basic steps are identical aside from the descriptor that defines this wallet, which is of the form: `wsh(thresh(4,pk(XPUB1),s:pk(XPUB2),s:pk(XPUB3),s:pk(XPUB4),sln:after(t1),sln:after(t2),sln:after(t3)))`.
|
||||
|
||||
[The test](/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py) is meant to be documentation as much as it is a functional test, so it is kept as simple and readable as possible.
|
||||
|
||||
### BIP32 derived keys and chains
|
||||
|
||||
Most modern wallet software and hardware uses keys that are derived using
|
||||
|
|
|
@ -275,6 +275,7 @@ BASE_SCRIPTS = [
|
|||
'mempool_truc.py',
|
||||
'wallet_txn_doublespend.py --legacy-wallet',
|
||||
'wallet_multisig_descriptor_psbt.py --descriptors',
|
||||
'wallet_miniscript_decaying_multisig_descriptor_psbt.py --descriptors',
|
||||
'wallet_txn_doublespend.py --descriptors',
|
||||
'wallet_backwards_compatibility.py --legacy-wallet',
|
||||
'wallet_backwards_compatibility.py --descriptors',
|
||||
|
|
136
test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py
Executable file
136
test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py
Executable file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2024 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test a miniscript multisig that starts as 4-of-4 and "decays" to 3-of-4, 2-of-4, and finally 1-of-4 at each future halvening block height.
|
||||
|
||||
Spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))`
|
||||
This is similar to `test/functional/wallet_multisig_descriptor_psbt.py`.
|
||||
"""
|
||||
|
||||
import random
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_approx,
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
|
||||
|
||||
class WalletMiniscriptDecayingMultisigDescriptorPSBTTest(BitcoinTestFramework):
|
||||
def add_options(self, parser):
|
||||
self.add_wallet_options(parser, legacy=False)
|
||||
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
self.setup_clean_chain = True
|
||||
self.wallet_names = []
|
||||
self.extra_args = [["-keypool=100"]]
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
self.skip_if_no_sqlite()
|
||||
|
||||
@staticmethod
|
||||
def _get_xpub(wallet, internal):
|
||||
"""Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
|
||||
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
|
||||
# keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
|
||||
# see section 'Key origin identification' in 'doc/descriptors.md' for more details...
|
||||
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
|
||||
|
||||
def create_multisig(self, external_xpubs, internal_xpubs):
|
||||
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every signer can do this."""
|
||||
self.node.createwallet(wallet_name=f"{self.name}", blank=True, descriptors=True, disable_private_keys=True)
|
||||
multisig = self.node.get_wallet_rpc(f"{self.name}")
|
||||
# spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))`
|
||||
# IMPORTANT: when backing up your descriptor, the order of key_1...key_4 must be correct!
|
||||
external = multisig.getdescriptorinfo(f"wsh(thresh({self.N},pk({'),s:pk('.join(external_xpubs)}),sln:after({'),sln:after('.join(map(str, self.locktimes))})))")
|
||||
internal = multisig.getdescriptorinfo(f"wsh(thresh({self.N},pk({'),s:pk('.join(internal_xpubs)}),sln:after({'),sln:after('.join(map(str, self.locktimes))})))")
|
||||
result = multisig.importdescriptors([
|
||||
{ # receiving addresses (internal: False)
|
||||
"desc": external["descriptor"],
|
||||
"active": True,
|
||||
"internal": False,
|
||||
"timestamp": "now",
|
||||
},
|
||||
{ # change addresses (internal: True)
|
||||
"desc": internal["descriptor"],
|
||||
"active": True,
|
||||
"internal": True,
|
||||
"timestamp": "now",
|
||||
},
|
||||
])
|
||||
assert all(r["success"] for r in result)
|
||||
return multisig
|
||||
|
||||
def run_test(self):
|
||||
self.node = self.nodes[0]
|
||||
self.M = 4 # starts as 4-of-4
|
||||
self.N = 4
|
||||
|
||||
self.locktimes = [104, 106, 108]
|
||||
assert_equal(len(self.locktimes), self.N - 1)
|
||||
|
||||
self.name = f"{self.M}_of_{self.N}_decaying_multisig"
|
||||
self.log.info(f"Testing a miniscript multisig which starts as 4-of-4 and 'decays' to 3-of-4 at block height {self.locktimes[0]}, 2-of-4 at {self.locktimes[1]}, and finally 1-of-4 at {self.locktimes[2]}...")
|
||||
|
||||
self.log.info("Create the signer wallets and get their xpubs...")
|
||||
signers = [self.node.get_wallet_rpc(self.node.createwallet(wallet_name=f"signer_{i}", descriptors=True)["name"]) for i in range(self.N)]
|
||||
external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in signers] for internal in [False, True]]
|
||||
|
||||
self.log.info("Create the watch-only decaying multisig using signers' xpubs...")
|
||||
multisig = self.create_multisig(external_xpubs, internal_xpubs)
|
||||
|
||||
self.log.info("Get a mature utxo to send to the multisig...")
|
||||
coordinator_wallet = self.node.get_wallet_rpc(self.node.createwallet(wallet_name="coordinator", descriptors=True)["name"])
|
||||
self.generatetoaddress(self.node, 101, coordinator_wallet.getnewaddress())
|
||||
|
||||
self.log.info("Send funds to the multisig's receiving address...")
|
||||
deposit_amount = 6.15
|
||||
coordinator_wallet.sendtoaddress(multisig.getnewaddress(), deposit_amount)
|
||||
self.generate(self.node, 1)
|
||||
assert_approx(multisig.getbalance(), deposit_amount, vspan=0.001)
|
||||
|
||||
self.log.info("Send transactions from the multisig as required signers decay...")
|
||||
amount = 1.5
|
||||
receiver = signers[0]
|
||||
sent = 0
|
||||
for locktime in [0] + self.locktimes:
|
||||
self.log.info(f"At block height >= {locktime} this multisig is {self.M}-of-{self.N}")
|
||||
current_height = self.node.getblock(self.node.getbestblockhash())['height']
|
||||
|
||||
# in this test each signer signs the same psbt "in series" one after the other.
|
||||
# Another option is for each signer to sign the original psbt, and then combine
|
||||
# and finalize these. In some cases this may be more optimal for coordination.
|
||||
psbt = multisig.walletcreatefundedpsbt(inputs=[], outputs={receiver.getnewaddress(): amount}, feeRate=0.00010, locktime=locktime)
|
||||
# the random sample asserts that any of the signing keys can sign for the 3-of-4,
|
||||
# 2-of-4, and 1-of-4. While this is basic behavior of the miniscript thresh primitive,
|
||||
# it is a critical property of this wallet.
|
||||
for i, m in enumerate(random.sample(range(self.M), self.M)):
|
||||
psbt = signers[m].walletprocesspsbt(psbt["psbt"])
|
||||
assert_equal(psbt["complete"], i == self.M - 1)
|
||||
|
||||
if self.M < self.N:
|
||||
self.log.info(f"Check that the time-locked transaction is too immature to spend with {self.M}-of-{self.N} at block height {current_height}...")
|
||||
assert_equal(current_height >= locktime, False)
|
||||
assert_raises_rpc_error(-26, "non-final", multisig.sendrawtransaction, psbt["hex"])
|
||||
|
||||
self.log.info(f"Generate blocks to reach the time-lock block height {locktime} and broadcast the transaction...")
|
||||
self.generate(self.node, locktime - current_height)
|
||||
else:
|
||||
self.log.info("All the signers are required to spend before the first locktime")
|
||||
|
||||
multisig.sendrawtransaction(psbt["hex"])
|
||||
sent += amount
|
||||
|
||||
self.log.info("Check that balances are correct after the transaction has been included in a block...")
|
||||
self.generate(self.node, 1)
|
||||
assert_approx(multisig.getbalance(), deposit_amount - sent, vspan=0.001)
|
||||
assert_equal(receiver.getbalance(), sent)
|
||||
|
||||
self.M -= 1 # decay the number of required signers for the next locktime..
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
WalletMiniscriptDecayingMultisigDescriptorPSBTTest(__file__).main()
|
Loading…
Add table
Reference in a new issue