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,