def test_ms_cli(dev, addr_fmt, clear_ms, import_ms_wallet, addr_vs_path, M=1, N=3): # exercise the p2sh command of ckcc:cli ... hard to do manually. from subprocess import check_output clear_ms() keys = import_ms_wallet(M, N, name='cli-test', accept=1) pmapper = lambda i: [HARD(45), i, 0,0] scr, pubkeys, xfp_paths = make_redeem(M, keys, pmapper) def decode_path(p): return '/'.join(str(i) if i < 0x80000000 else "%d'"%(i& 0x7fffffff) for i in p) for mode in [ 'full', 'prefix']: args = ['ckcc'] if dev.is_simulator: args += ['-x'] args += ['p2sh', '-q'] if addr_fmt == AF_P2WSH: args += ['-s'] elif addr_fmt == AF_P2WSH_P2SH: args += ['-s', '-w'] if mode == 'full': args += ['-p', "", B2A(scr)] args += [xfp2str(x)+'/'+decode_path(path) for x,*path in xfp_paths] else: args += [B2A(scr)] args += [xfp2str(x)+'/'+decode_path(path[1:]) for x,*path in xfp_paths] print('CMD: ' + (' '.join(args))) addr = check_output(args, encoding='ascii').strip() print(addr) addr_vs_path(addr, addr_fmt=addr_fmt, script=scr) # test case for make_ms_address really. expect_addr, scr2, _ = make_ms_address(M, keys, path_mapper=pmapper, addr_fmt=addr_fmt) assert expect_addr == addr assert scr2 == scr # need to re-start our connection once ckcc has talked to simulator dev.start_encryption() dev.check_mitm() clear_ms()
def hack(psbt): # change all inputs to be "not ours" ... but with utxo details for idx, i in enumerate(psbt.inputs): for pubkey in i.bip32_paths: here = struct.pack('<I', idx) i.bip32_paths[pubkey] = here + i.bip32_paths[pubkey][4:] wrongs.add(xfp2str(idx))
def test_import_seed(goto_home, pick_menu_item, cap_story, need_keypress, unit_test, cap_menu, word_menu_entry, seed_words, xfp, get_secrets, reset_seed_words, cap_screen_qr, qr_quality_check): unit_test('devtest/clear_seed.py') m = cap_menu() assert m[0] == 'New Wallet' pick_menu_item('Import Existing') sw = seed_words.split(' ') pick_menu_item('%d Words' % len(sw)) word_menu_entry(sw) m = cap_menu() assert m[0] == 'Ready To Sign' pick_menu_item('Advanced') pick_menu_item('View Identity') title, body = cap_story() assert ' '+xfp2str(xfp) in body v = get_secrets() assert 'Press 3 to show QR code' in body need_keypress('3') qr = cap_screen_qr().decode('ascii') assert qr == v['xpub'] assert v['mnemonic'] == seed_words reset_seed_words()
def test_import_seed(goto_home, pick_menu_item, cap_story, need_keypress, unit_test, cap_menu, word_menu_entry, seed_words, xfp, get_secrets, reset_seed_words): unit_test('devtest/clear_seed.py') m = cap_menu() assert m[0] == 'New Wallet' pick_menu_item('Import Existing') sw = seed_words.split(' ') pick_menu_item('%d Words' % len(sw)) word_menu_entry(sw) m = cap_menu() assert m[0] == 'Ready To Sign' pick_menu_item('Advanced') pick_menu_item('View Identity') title, body = cap_story() assert ' '+xfp2str(xfp) in body v = get_secrets() assert v['mnemonic'] == seed_words reset_seed_words()
def doit(M, keys, addr_fmt=AF_P2SH, bip45=True, **make_redeem_args): # test we are showing addresses correctly # - verifies against bitcoind as well addr_fmt = unmap_addr_fmt.get(addr_fmt, addr_fmt) # make a redeem script, using provided keys/pubkeys if bip45: make_redeem_args['path_mapper'] = lambda i: [HARD(45), i, 0,0] scr, pubkeys, xfp_paths = make_redeem(M, keys, **make_redeem_args) assert len(scr) <= 520, "script too long for standard!" got_addr = dev.send_recv(CCProtocolPacker.show_p2sh_address( M, xfp_paths, scr, addr_fmt=addr_fmt), timeout=None) title, story = cap_story() #print(story) assert got_addr in story assert all((xfp2str(xfp) in story) for xfp,_,_ in keys) if bip45: for i in range(len(keys)): assert ('/_/%d/0/0' % i) in story need_keypress('y') # check expected addr was generated based on my math addr_vs_path(got_addr, addr_fmt=addr_fmt, script=scr) # also check against bitcoind core_addr, core_scr = bitcoind_p2sh(M, pubkeys, addr_fmt) assert B2A(scr) == core_scr assert core_addr == got_addr
def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, keys=None): keys = keys or make_multisig(M, N, unique=unique) # render as a file for import name = name or f'test-{M}-{N}' config = f"name: {name}\npolicy: {M} / {N}\n\n" if addr_fmt: config += f'format: {addr_fmt.title()}\n' if common: config += f'derivation: {common}\n' config += '\n'.join('%s: %s' % (xfp2str(xfp), dd.hwif(as_private=False)) for xfp, m, dd in keys) print(config) title, story = offer_ms_import(config) assert 'Create new multisig' in story assert name in story assert f'Policy: {M} of {N}\n' in story if accept: time.sleep(.1) need_keypress('y') # Test it worked. time.sleep(.1) # required xor = 0 for xfp, _, _ in keys: xor ^= xfp assert dev.send_recv(CCProtocolPacker.multisig_check(M, N, xor)) == 1 return keys
def test_export_public_txt(dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, addr_vs_path): from pycoin.contrib.segwit_addr import encode as sw_encode # test UX and values produced. goto_home() pick_menu_item('Advanced') pick_menu_item('MicroSD Card') pick_menu_item('Dump Summary') time.sleep(0.1) title, story = cap_story() assert 'Saves a text file to' in story need_keypress('y') time.sleep(0.1) title, story = cap_story() assert 'Summary file' in story fname = story.split('\n')[-1] assert 'public' in fname xfp = xfp2str(simulator_fixed_xfp).upper() root = BIP32Node.from_wallet_key(simulator_fixed_xprv) path = microsd_path(fname) with open(path, 'rt') as fp: for ln in fp.readlines(): if 'fingerprint' in ln: assert ln.strip().endswith(xfp) if '=>' not in ln: continue lhs, rhs = ln.strip().split(' => ') assert lhs.startswith('m/') rhs = rhs.split('#')[0].strip() if 'SLIP-132' in ln: rhs, _, f, _ = slip132undo(rhs) else: f = None if rhs[1:4] == 'pub': expect = root.subkey_for_path(lhs[2:]) assert expect.hwif(as_private=False) == rhs continue if not f: if rhs[0] in 'mn': f = AF_CLASSIC elif rhs[0:3] == 'tb1': f = AF_P2WPKH elif rhs[0] == '2': f = AF_P2WPKH_P2SH else: raise ValueError(rhs) addr_vs_path(rhs, path=lhs, addr_fmt=f)
def doit(M, addr_fmt=None, do_import=True): passwords = ['Me', 'Myself', 'And I', ''] if 0: # WORKING, but slow .. and it's constant data keys = [] for pw in passwords: xfp = set_bip39_pw(pw) sk = dev.send_recv(CCProtocolPacker.get_xpub("m/45'")) node = BIP32Node.from_wallet_key(sk) keys.append((xfp, None, node)) assert len(set(x for x,_,_ in keys)) == 4, keys pprint(keys) else: # Much, FASTER! assert dev.is_simulator keys = [(3503269483, None, BIP32Node.from_hwif('tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9')), (2389277556, None, BIP32Node.from_hwif('tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc')), (3190206587, None, BIP32Node.from_hwif('tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa')), (1130956047, None, BIP32Node.from_hwif('tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n')), ] if do_import: # render as a file for import config = f"name: Myself-{M}\npolicy: {M} / 4\n\n" if addr_fmt: config += f'format: {addr_fmt.upper()}\n' config += '\n'.join('%s: %s' % (xfp2str(xfp), sk.hwif()) for xfp, _, sk in keys) #print(config) title, story = offer_ms_import(config) #print(story) # dont care if update or create; accept it. time.sleep(.1) need_keypress('y') def select_wallet(idx): # select to specific pw xfp = set_bip39_pw(passwords[idx]) assert xfp == keys[idx][0] return (keys, select_wallet)
def test_export_wasabi(dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path): # test UX and operation of the 'wasabi wallet export' goto_home() pick_menu_item('Advanced') pick_menu_item('MicroSD Card') pick_menu_item('Export Wallet') pick_menu_item('Wasabi Wallet') time.sleep(0.1) title, story = cap_story() assert 'This saves a skeleton Wasabi' in story need_keypress('y') time.sleep(0.1) title, story = cap_story() assert 'wallet file written' in story fname = story.split('\n')[-1] need_keypress('y') path = microsd_path(fname) with open(path, 'rt') as fp: obj = json.load(fp) assert 'MasterFingerprint' in obj assert 'ExtPubKey' in obj assert 'ColdCardFirmwareVersion' in obj xpub = obj['ExtPubKey'] assert xpub.startswith('xpub') # even for testnet assert obj['MasterFingerprint'] == xfp2str(simulator_fixed_xfp) got = BIP32Node.from_wallet_key(xpub) expect = BIP32Node.from_wallet_key( simulator_fixed_xprv).subkey_for_path("84'/0'/0'.pub") assert got.sec() == expect.sec() os.unlink(path)
def test_make_example_file(N, microsd_path, make_multisig, addr_fmt=None): M=3 keys = make_multisig(M, N) # render as a file for import name = f'sample-{M}-{N}' config = f"name: {name}\npolicy: {M} / {N}\n\n" if addr_fmt: config += f'format: {addr_fmt.upper()}\n' config += '\n'.join('%s: %s' % (xfp2str(xfp), sk.hwif(as_private=False)) for xfp,m,sk in keys) fname = microsd_path(f'{name}.txt') with open(fname, 'wt') as fp: fp.write(config+'\n') print(f"Created: {fname}")
def test_export_unchained(dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path): # test UX and operation of the 'unchained capital export' goto_home() pick_menu_item('Advanced') pick_menu_item('MicroSD Card') pick_menu_item('Export Wallet') pick_menu_item('Unchained Capital') time.sleep(0.1) title, story = cap_story() assert 'Unchained Capital' in story need_keypress('y') time.sleep(0.1) title, story = cap_story() assert 'Unchained Capital file' in story fname = story.split('\n')[-1] assert 'unchained' in fname need_keypress('y') root = BIP32Node.from_wallet_key(simulator_fixed_xprv) path = microsd_path(fname) with open(path, 'rt') as fp: obj = json.load(fp) assert obj['xfp'] == xfp2str(simulator_fixed_xfp) assert obj['account'] == 0 assert obj['p2sh_deriv'] == "m/45'" for k in ['p2sh_p2wsh', 'p2sh', 'p2wsh']: xpub = slip132undo(obj[k])[0] if k != 'p2sh' else obj[k] node = BIP32Node.from_wallet_key(xpub) assert xpub == node.hwif(as_private=False) sk = root.subkey_for_path(obj[f'{k}_deriv'][2:] + '.pub') #assert node.chain_code() == sk.chain_code() assert node.hwif() == sk.hwif()
def make_named(name): config = f"name: {name}\npolicy: {M} / {N}\n\n" config += '\n'.join('%s: %s' % (xfp2str(xfp), sk.hwif(as_private=False)) for xfp,m,sk in keys) return config
def test_bitcoind_cosigning(dev, bitcoind, start_sign, end_sign, import_ms_wallet, clear_ms, explora, try_sign, need_keypress, addr_style): # Make a P2SH wallet with local bitcoind as a co-signer (and simulator) # - send an receive various # - following text of <https://github.com/bitcoin/bitcoin/blob/master/doc/psbt.md> # - the constructed multisig walelt will only work for a single pubkey on core side # - before starting this test, have some funds already deposited to bitcoind testnet wallet from pycoin.encoding import sec_to_public_pair from binascii import a2b_hex import re if addr_style == 'legacy': addr_fmt = AF_P2SH elif addr_style == 'p2sh-segwit': addr_fmt = AF_P2WSH_P2SH elif addr_style == 'bech32': addr_fmt = AF_P2WSH try: addr, = bitcoind.getaddressesbylabel("sim-cosign").keys() except: addr = bitcoind.getnewaddress("sim-cosign") info = bitcoind.getaddressinfo(addr) #pprint(info) assert info['address'] == addr bc_xfp = swab32(int(info['hdmasterfingerprint'], 16)) bc_deriv = info['hdkeypath'] # example: "m/0'/0'/3'" bc_pubkey = info['pubkey'] # 02f75ae81199559c4aa... pp = sec_to_public_pair(a2b_hex(bc_pubkey)) # No means to export XPUB from bitcoind! Still. In 2019. # - this fake will only work for for one pubkey value, the first/topmost node = BIP32Node('XTN', b'\x23'*32, depth=len(bc_deriv.split('/'))-1, parent_fingerprint=a2b_hex('%08x' % bc_xfp), public_pair=pp) keys = [ (bc_xfp, None, node), (1130956047, None, BIP32Node.from_hwif('tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n')), # simulator: m/45' ] M,N=2,2 clear_ms() import_ms_wallet(M, N, keys=keys, accept=1, name="core-cosign") cc_deriv = "m/45'/55" cc_pubkey = B2A(BIP32Node.from_hwif(simulator_fixed_xprv).subkey_for_path(cc_deriv[2:]).sec()) # NOTE: bitcoind doesn't seem to implement pubkey sorting. We have to do it. resp = bitcoind.addmultisigaddress(M, list(sorted([cc_pubkey, bc_pubkey])), 'shared-addr-'+addr_style, addr_style) ms_addr = resp['address'] bc_redeem = a2b_hex(resp['redeemScript']) assert bc_redeem[0] == 0x52 def mapper(cosigner_idx): return list(str2ipath(cc_deriv if cosigner_idx else bc_deriv)) scr, pubkeys, xfp_paths = make_redeem(M, keys, mapper) assert scr == bc_redeem # check Coldcard calcs right address to match got_addr = dev.send_recv(CCProtocolPacker.show_p2sh_address( M, xfp_paths, scr, addr_fmt=addr_fmt), timeout=None) assert got_addr == ms_addr time.sleep(.1) need_keypress('x') # clear screen / start over print(f"Will be signing an input from {ms_addr}") if xfp2str(bc_xfp) in ('5380D0ED', 'EDD08053'): # my own expected values assert ms_addr in ( '2NDT3ymKZc8iMfbWqsNd1kmZckcuhixT5U4', '2N1hZJ5mazTX524GQTPKkCT4UFZn5Fqwdz6', 'tb1qpcv2rkc003p5v8lrglrr6lhz2jg8g4qa9vgtrgkt0p5rteae5xtqn6njw9') # Need some UTXO to sign # # - but bitcoind can't give me that (using listunspent) because it's only a watched addr?? # did_fund = False while 1: rr = explora('address', ms_addr, 'utxo') pprint(rr) avail = [] amt = 0 for i in rr: txn = i['txid'] vout = i['vout'] avail.append( (txn, vout) ) amt += i['value'] # just use first UTXO available; save other for later tests break else: # doesn't need to confirm, but does need to reach public testnet/blockstream assert not amt and not avail if not did_fund: print(f"Sending some XTN to {ms_addr} (wait)") bitcoind.sendtoaddress(ms_addr, 0.0001, 'fund testing') did_fund = True else: print(f"Still waiting ...") time.sleep(2) if amt: break ret_addr = bitcoind.getrawchangeaddress() ''' If you get insufficent funds, even tho we provide the UTXO (!!), do this: bitcoin-cli importaddress "2NDT3ymKZc8iMfbWqsNd1kmZckcuhixT5U4" true true Better method: always fund addresses for testing here from same wallet (ie. got from non-multisig to multisig on same bitcoin-qt instance). -> Now doing that, automated, above. ''' resp = bitcoind.walletcreatefundedpsbt([dict(txid=t, vout=o) for t,o in avail], [{ret_addr: amt/1E8}], 0, {'subtractFeeFromOutputs': [0], 'includeWatching': True}, True) assert resp['changepos'] == -1 psbt = b64decode(resp['psbt']) open('debug/funded.psbt', 'wb').write(psbt) # patch up the PSBT a little ... bitcoind doesn't know the path for the CC's key ex = BasicPSBT().parse(psbt) cxpk = a2b_hex(cc_pubkey) for i in ex.inputs: assert cxpk in i.bip32_paths, 'input not to be signed by CC?' i.bip32_paths[cxpk] = pack('<3I', keys[1][0], *str2ipath(cc_deriv)) psbt = ex.as_bytes() open('debug/patched.psbt', 'wb').write(psbt) _, updated = try_sign(psbt, finalize=False) open('debug/cc-updated.psbt', 'wb').write(updated) # have bitcoind do the rest of the signing rr = bitcoind.walletprocesspsbt(b64encode(updated).decode('ascii')) pprint(rr) open('debug/bc-processed.psbt', 'wt').write(rr['psbt']) assert rr['complete'] # finalize and send rr = bitcoind.finalizepsbt(rr['psbt'], True) open('debug/bc-final-txn.txn', 'wt').write(rr['hex']) assert rr['complete'] txn_id = bitcoind.sendrawtransaction(rr['hex']) print(txn_id)
def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, bitcoind_wallet): # test UX and operation of the 'bitcoin core' wallet export from pycoin.contrib.segwit_addr import encode as sw_encode goto_home() pick_menu_item('Advanced') pick_menu_item('MicroSD Card') pick_menu_item('Export Wallet') pick_menu_item('Bitcoin Core') time.sleep(0.1) title, story = cap_story() assert 'This saves' in story assert 'run that command' in story assert 'Press 1 to' in story if acct_num is not None: need_keypress('1') time.sleep(0.1) for n in acct_num: need_keypress(n) else: acct_num = '0' need_keypress('y') time.sleep(0.1) title, story = cap_story() assert 'Bitcoin Core file written' in story fname = story.split('\n')[-1] need_keypress('y') path = microsd_path(fname) addrs = [] js = None with open(path, 'rt') as fp: for ln in fp: if 'importmulti' in ln: assert ln.startswith("importmulti '") assert ln.endswith("'\n") assert not js, "dup importmulti lines" js = ln[13:-2] elif '=>' in ln: path, addr = ln.strip().split(' => ', 1) assert path.startswith(f"m/84'/1'/{acct_num}'/0") assert addr.startswith('tb1q') sk = BIP32Node.from_wallet_key( simulator_fixed_xprv).subkey_for_path(path[2:]) h20 = sk.hash160() assert addr == sw_encode(addr[0:2], 0, h20) addrs.append(addr) assert len(addrs) == 3 obj = json.loads(js) xfp = xfp2str(simulator_fixed_xfp).lower() for n, here in enumerate(obj): assert here['range'] == [0, 1000] assert here['timestamp'] == 'now' assert here['internal'] == bool(n) assert here['keypool'] == True assert here['watchonly'] == True d = here['desc'] desc, chk = d.split('#', 1) assert len(chk) == 8 assert desc.startswith(f'wpkh([{xfp}/84h/1h/{acct_num}h]') expect = BIP32Node.from_wallet_key(simulator_fixed_xprv)\ .subkey_for_path(f"84'/1'/{acct_num}'.pub").hwif() assert expect in desc assert expect + f'/{n}/*' in desc # test against bitcoind for x in obj: x['label'] = 'testcase' bitcoind_wallet.importmulti(obj) x = bitcoind_wallet.getaddressinfo(addrs[-1]) from pprint import pprint pprint(x) assert x['address'] == addrs[-1] assert x['label'] == 'testcase' assert x['iswatchonly'] == True assert x['iswitness'] == True assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs) - 1)
def test_export_xpub(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, enter_number, cap_screen_qr, use_mainnet): # XPUB's via QR use_mainnet() goto_home() pick_menu_item('Advanced') pick_menu_item('Export XPUB') top_items = cap_menu() for m in top_items: is_xfp = False if '-84' in m: expect = "m/84'/0'/{acct}'" elif '-44' in m: expect = "m/44'/0'/{acct}'" elif '49' in m: expect = "m/49'/0'/{acct}'" elif 'Master' in m: expect = "m" elif 'XFP' in m: is_xfp = True pick_menu_item(m) time.sleep(0.1) if is_xfp: got = cap_screen_qr().decode('ascii') assert got == xfp2str(simulator_fixed_xfp).upper() need_keypress('x') continue title, story = cap_story() assert expect in story if 'acct' in expect: assert "Press 1 to select account" in story if acct_num is not None: need_keypress('1') enter_number(acct_num) time.sleep(0.1) expect = expect.format(acct=acct_num) title, story = cap_story() assert expect in story assert "Press 1 to select account" not in story expect = expect.format(acct=0) need_keypress('y') got_pub = cap_screen_qr().decode('ascii') if got_pub[0] not in 'xt': got_pub,*_ = slip132undo(got_pub) got = BIP32Node.from_wallet_key(got_pub) wallet = BIP32Node.from_wallet_key(simulator_fixed_xprv) if expect != 'm': wallet = wallet.subkey_for_path(expect[2:]) assert got.sec() == wallet.sec() need_keypress('x')