diff --git a/docs/limitations.md b/docs/limitations.md index bfa32523..799576cb 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -75,7 +75,10 @@ # Taproot -- taproot limitation are listed in `docs/taproot.md` +- only `TREE` of depth 0 is allowed +- max 32 signers in TR multisig, only allowed script is: `sortedmulti_a` +- if we can sign by both key path and script path: key path has precedence. +- more background and detail in `docs/taproot.md` # SIGHASH types diff --git a/docs/taproot.md b/docs/taproot.md index 7562a4e9..42bd5bfd 100644 --- a/docs/taproot.md +++ b/docs/taproot.md @@ -1,10 +1,10 @@ # Taproot -**COLDCARD®** Mk4 experimental `EDGE` version `5.2.0X` +**COLDCARD®** Mk4 experimental `EDGE` versions will support Schnorr signatures ([BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)), Taproot ([BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) and very limited Tapscript ([BIP-0342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki)) support. -Tapscript support will get more versatile with next iterations. +Tapscript support will get more versatile with future iterations. ## Output script (a.k.a address) generation @@ -40,7 +40,7 @@ for multisig. ## Limitations -### Tapscript limitations +### Tapscript Limitations In current version only `TREE` of depth 0 is allowed. Meaning that only one leaf script is allowed and tagged hash of this single leaf script is also a merkle root. Only allowed script (for now) is `sortedmulti_a`. @@ -48,7 +48,7 @@ Taproot multisig currently has artificial limit of max 32 signers (M=N=32). If Coldcard can sign by both key path and script path - key path has precedence. -### PSBT limitations +### PSBT Requirements PSBT provider MUST provide following Taproot specific input fields in PSBT: 1. `PSBT_IN_TAP_BIP32_DERIVATION` with all the necessary keys with their leaf hashes and derivation (including XFP). Internal key has to be specified here with empty leaf hashes. diff --git a/shared/multisig.py b/shared/multisig.py index 7c85b631..35f33119 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -8,7 +8,8 @@ from utils import str_to_keypath, problem_file_line, export_prompt_builder, pars from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys, ux_enter_bip32_index from files import CardSlot, CardMissingError, needs_microsd from descriptor import MultisigDescriptor, multisig_descriptor_template -from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS, AF_P2TR +from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_SIGNERS +from public_constants import MAX_TR_SIGNERS, AF_P2TR from menu import MenuSystem, MenuItem from opcodes import OP_CHECKMULTISIG, OP_CHECKSIG, OP_NUMEQUAL, OP_CHECKSIGADD from exceptions import FatalPSBTIssue @@ -28,7 +29,7 @@ class MultisigOutOfSpace(RuntimeError): pass def disassemble_multisig_mn(redeem_script): - # pull out just M and N from script. Simple, faster, no memory. + # Pull out just M and N from script. Simple, faster, no memory. assert redeem_script[-1] == OP_CHECKMULTISIG, 'need CHECKMULTISIG' @@ -39,8 +40,8 @@ def disassemble_multisig_mn(redeem_script): def disassemble_multisig_mn_tr(script): - # pull out just M and N from script. Simple, faster, no memory. - # more validation is done in next steps + # Pull out just M and N from taproot script. + # - more validation is done in following steps assert script[-1] == OP_NUMEQUAL, 'need OP_NUMEQUAL' num_cs = 0 num_csa = 0 @@ -68,6 +69,7 @@ def disassemble_multisig_mn_tr(script): last = next(gen)[1] assert last == OP_NUMEQUAL M = int.from_bytes(bt[0], "little") + assert M N = num_cs + num_csa return M, N @@ -142,15 +144,15 @@ def make_redeem_script(M, nodes, subkey_idx): def make_redeem_script_tr(M, nodes, subkey_idx): - # take a list of BIP-32 nodes, and derive Nth subkey (subkey_idx) and make + # Take a list of BIP-32 nodes, and derive Nth subkey (subkey_idx) and make # a taproot M-of-N redeem script for that. Always applies BIP-67 sorting. + # - tapscript multisig does not use OP_CHECKMULTISIG and therefore limit is + # much higher (998 of 999 was demonstrated) + # - for now, MAX_TR_SIGNERS is 32, but this is artificial limit for tapscript + # and could be something bigger + N = len(nodes) - # TODO this is artificial limit for tapscript and should be something bigger - # MAX_SIGNERS is actually old P2SH limit - # limit for segwit v0 multisig is 20 (OP_CHECKMULTISIG limit) - # tapscript multisig does not use OP_CHECKMULTISIG and therefore limit is much higher (998of999 was tried) - # double current limit - assert 1 <= M <= N <= 32 + assert 1 <= M <= N <= MAX_TR_SIGNERS pubkeys = [] for n in nodes: @@ -173,7 +175,7 @@ def make_redeem_script_tr(M, nodes, subkey_idx): if M <= 16: script += bytes([80 + M, OP_NUMEQUAL]) else: - # up to 127 + assert M < 128 script += bytes([0x01, M, OP_NUMEQUAL]) return script @@ -187,7 +189,7 @@ class MultisigWallet: # - required during signing to verify change outputs # - can reconstruct any redeem script from this # Challenges: - # - can be big, taking big % of 4k storage in nvram + # - can be big, taking big % of storage in nvram # - complex object, want to have flexibility going forward FORMAT_NAMES = [ (AF_P2SH, 'p2sh'), @@ -581,6 +583,7 @@ class MultisigWallet: ch = chains.current_chain() internal_key = None xfp_deriv = None + for key, lhs_path in taproot_subpaths.items(): if not lhs_path[0]: internal_key = key @@ -601,6 +604,7 @@ class MultisigWallet: return internal_key def make_multisig_tr(self, taproot_subpaths): + # Make the redeem script for leafs ch = chains.current_chain() index = None nodes = [] @@ -626,8 +630,7 @@ class MultisigWallet: nodes.append(node) # this assumes we have same index for all keys - script = make_redeem_script_tr(self.M, nodes, index) - return script + return make_redeem_script_tr(self.M, nodes, index) def validate_script(self, redeem_script, subpaths=None, xfp_paths=None): # Check we can generate all pubkeys in the redeem script, raise on errors. @@ -645,7 +648,8 @@ class MultisigWallet: M, N, pubkeys = disassemble_multisig(redeem_script) assert M==self.M and N == self.N, 'wrong M/N in script' - if self.disable_checks: return ['UNVERIFIED'] + if self.disable_checks: + return ['UNVERIFIED'] for pk_order, pubkey in enumerate(pubkeys): check_these = [] @@ -743,6 +747,7 @@ class MultisigWallet: xpubs = [] addr_fmt = AF_P2SH my_xfp = settings.get('xfp') + for ln in lines: # remove comments comm = ln.find('#') @@ -811,6 +816,7 @@ class MultisigWallet: is_mine = cls.check_xpub(xfp, value, deriv, chains.current_chain().ctype, my_xfp, xpubs) if is_mine: has_mine += 1 + return name, addr_fmt, xpubs, has_mine, M, N @classmethod @@ -826,6 +832,7 @@ class MultisigWallet: is_mine = cls.check_xpub(xfp, xpub, deriv, chains.current_chain().ctype, my_xfp, xpubs) if is_mine: has_mine += 1 + return None, desc.addr_fmt, xpubs, has_mine, desc.M, desc.N, desc.internal_key def to_descriptor(self): diff --git a/testing/test_multisig.py b/testing/test_multisig.py index 16f80275..eed27f4b 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -2540,7 +2540,7 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, @pytest.mark.bitcoind @pytest.mark.parametrize("change", [True, False]) -@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5), (15, 15)]) +@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5), (32, 32)]) @pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) def test_bitcoind_taproot_ms_address(change, M_N, clear_ms, goto_home, need_keypress, pick_menu_item, cap_menu, cap_story, make_multisig, import_ms_wallet, microsd_path, bitcoind_multisig, @@ -2704,7 +2704,7 @@ def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wa @pytest.mark.parametrize('cmn_pth_from_root', [True, False]) @pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) -@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5), (15, 15)]) +@pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5), (32, 32)]) @pytest.mark.parametrize('addr_fmt', [AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH]) def test_multisig_descriptor_export(M_N, way, addr_fmt, cmn_pth_from_root, clear_ms, make_multisig, import_ms_wallet, goto_home, pick_menu_item, cap_menu,