From 12f62f95bc9a8d44e17fc1aaade35cf85a6f0560 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 23 Oct 2024 16:36:47 +0200 Subject: [PATCH] unique ms names from ccc feature; multiple ms wallets test --- shared/ccc.py | 7 +- shared/multisig.py | 17 ++- testing/test_ccc.py | 261 ++++++++++++++++++++++++++------------------ 3 files changed, 171 insertions(+), 114 deletions(-) diff --git a/shared/ccc.py b/shared/ccc.py index cf899afa..3f22c1c6 100644 --- a/shared/ccc.py +++ b/shared/ccc.py @@ -265,7 +265,7 @@ class CCCConfigMenu(MenuSystem): items.append(MenuItem('↳ %d/%d: %s' % (ms.M, ms.N, ms.name), menu=make_ms_wallet_menu, arg=ms.storage_idx)) - items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN)) + items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=len(items)>4)) if CCCFeature.last_fail_reason: # xxxxxxxxxxxxxxxx @@ -324,7 +324,8 @@ wallet, proceed to the multisig menu and remove related wallet entry.'''): from multisig import export_multisig_xpubs await export_multisig_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True) - async def build_2ofN(self, *a): + async def build_2ofN(self, m, l, i): + num = i.arg # ask for a key B, assume A and C are defined => export MS config and import into self. # - like the airgap setup, but assume A and C are this Coldcard m = '''Builds simple 2-of-N multisig wallet, with this Coldcard's main secret (key A), \ @@ -337,7 +338,7 @@ be ready to show it as a QR, before proceeding.''' from multisig import create_ms_step1 # picks addr fmt, QR or not, gets at least one file, then... - await create_ms_step1(for_ccc=CCCFeature.get_encoded_secret()) + await create_ms_step1(for_ccc=(CCCFeature.get_encoded_secret(), num)) # prompt for file, prompt for our acct number, unless already exported to this card? diff --git a/shared/multisig.py b/shared/multisig.py index 2613762e..6251e139 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -1738,13 +1738,14 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False, return if for_ccc: + secret, rand_name = for_ccc # Always include 2 keys from CCC: own master (key A) and key C # - force them to same derivation. acct = await ux_enter_bip32_index('CCC Account Number:') or 0 dis.fullscreen("Wait...") a = add_own_xpub(chain, acct, addr_fmt) # master: key A - c = add_own_xpub(chain, acct, addr_fmt, secret=for_ccc) + c = add_own_xpub(chain, acct, addr_fmt, secret=secret) # problem: above file searching may find xpub export from key C # (or our master seed, exported) .. we can't add them again, @@ -1791,7 +1792,10 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False, assert 1 <= M <= N <= MAX_SIGNERS if for_ccc: - name = 'Coldcard Cosign' + name = "Coldcard Co-sign" if version.has_qwerty else "CCC" + if rand_name: + # unique name (hopefully) for each CCC wallet + name += "x%X" % ngu.random.bytes(1)[0] else: name = 'CC-%d-of-%d' % (M, N) @@ -1818,14 +1822,15 @@ async def create_ms_step1(*a, for_ccc=None): if version.has_qr: # They have a scanner, could do QR codes... - ch = await ux_show_story("Press "+ KEY_QR + " to scan multisg XPUBs from "\ - "QR codes (BBQr) or ENTER to use SD card(s).", title="QR or SD Card?") + ch = await ux_show_story("Press "+ KEY_QR + " to scan multisg XPUBs from " + "QR codes (BBQr) or ENTER to use SD card(s).", + title="QR or SD Card?") if ch == KEY_QR: is_qr = True - ch = await ux_show_story("Press ENTER for default address format (P2WSH, segwit), "\ + ch = await ux_show_story("Press ENTER for default address format (P2WSH, segwit), " "otherwise, press (1) for P2SH-P2WSH.", title="Address Format", - escape="1") + escape="1") else: ch = await ux_show_story('''\ diff --git a/testing/test_ccc.py b/testing/test_ccc.py index c7d378a0..62fd2c8a 100644 --- a/testing/test_ccc.py +++ b/testing/test_ccc.py @@ -5,8 +5,7 @@ # run simulator without --eff # # -import pytest, pdb, requests, re, time, random, json, glob, os, hashlib, struct, base64 -from binascii import a2b_hex +import pytest, pdb, requests, re, time, random, json, glob, os, hashlib, base64 from base64 import urlsafe_b64encode from onetimepass import get_totp from helpers import prandom, slip132undo @@ -369,35 +368,43 @@ def enter_enabled_ccc(goto_home, pick_menu_item, cap_story, press_select, is_q1, @pytest.fixture def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_menu_item, cap_story, press_select, need_keypress, enter_number): - def doit(b_words=12, way="sd", addr_fmt=AF_P2WSH): - if isinstance(b_words, int): - assert b_words in (12,24) - words = Mnemonic('english').generate(strength=128 if b_words == 12 else 256) - b39_seed = Mnemonic.to_seed(words) - else: - assert isinstance(b_words, list) - b39_seed = Mnemonic.to_seed(" ".join(b_words)) + def doit(N=3, b_words=12, way="sd", addr_fmt=AF_P2WSH): - master = BIP32Node.from_master_secret(b39_seed) - xfp = master.fingerprint().hex().upper() - label = "p2wsh" if addr_fmt == AF_P2WSH else "p2sh_p2wsh" - derive = f"m/48h/1h/0h/{'2' if addr_fmt == AF_P2WSH else '1'}h" - derived = master.subkey_for_path(derive) + N2 = N - 2 # how many more signers we need (B keys) + + res = [] + for i in range(N2): + if isinstance(b_words, int): + assert b_words in (12,24) + words = Mnemonic('english').generate(strength=128 if b_words == 12 else 256) + b39_seed = Mnemonic.to_seed(words) + else: + assert isinstance(b_words, list) + b39_seed = Mnemonic.to_seed(" ".join(b_words)) + + master = BIP32Node.from_master_secret(b39_seed) + xfp = master.fingerprint().hex().upper() + label = "p2wsh" if addr_fmt == AF_P2WSH else "p2sh_p2wsh" + derive = f"m/48h/1h/0h/{'2' if addr_fmt == AF_P2WSH else '1'}h" + derived = master.subkey_for_path(derive) + + data = { + f"{label}_deriv": derive, + f"{label}": derived.hwif(), + "account": "0", + "xfp": xfp + } + res.append((derived, data)) - data = json.dumps({ - f"{label}_deriv": derive, - f"{label}": derived.hwif(), - "account": "0", - "xfp": xfp - }) if way in ("sd", "vdisk"): path_f = microsd_path if way == "sd" else virtdisk_path for fn in glob.glob(path_f('ccxp-*.json')): os.remove(fn) # cleanup as we want to control N - fname = f"ccxp-{xfp}.json" - with open(path_f(fname), "w") as f: - f.write(data) + for d, dd in res: + fname = f"ccxp-{dd['xfp']}.json" + with open(path_f(fname), "w") as f: + f.write(json.dumps(dd)) m = cap_menu() target_mi = None @@ -423,10 +430,11 @@ def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_m press_select() else: need_keypress(KEY_QR) - _, parts = split_qrs(data, 'J', max_version=20) - for p in parts: - scan_a_qr(p) - time.sleep(.1) + for d, dd in res: + _, parts = split_qrs(json.dumps(dd), 'J', max_version=20) + for p in parts: + scan_a_qr(p) + time.sleep(.1) # casual on-device multisig create if addr_fmt == AF_P2WSH: @@ -439,12 +447,21 @@ def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_m time.sleep(.1) title, story = cap_story() assert "Create new multisig wallet" in story - assert "Policy: 2 of 3" in story - assert "Coldcard Cosign" in story + assert f"Policy: 2 of {N}" in story + if is_q1: + assert "Coldcard Co-sign" in story + else: + assert "CCC" in story press_select() + time.sleep(.1) - # something that we need for fake_ms_tx - return struct.unpack('