1282 lines
42 KiB
Python
1282 lines
42 KiB
Python
# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
import pytest, time, os, base64
|
|
|
|
from conftest import microsd_path
|
|
from helpers import prandom, addr_from_display_format
|
|
from charcodes import KEY_QR, KEY_NFC, KEY_UP
|
|
from constants import unmap_addr_fmt, AF_P2WSH, AF_P2SH
|
|
from bip32 import BIP32Node, PrivateKey
|
|
from base58 import encode_base58_checksum
|
|
from msg import verify_message, parse_signed_message
|
|
from psbt import BasicPSBT
|
|
from helpers import str_to_path
|
|
|
|
|
|
def make_fake_wif(prefix=239):
|
|
# generate a WIF
|
|
return encode_base58_checksum(bytes([prefix]) + prandom(32) + b'\x01')
|
|
|
|
@pytest.mark.parametrize("num_wifs", [1, 11])
|
|
@pytest.mark.parametrize("separator", ["\n", ','])
|
|
@pytest.mark.parametrize("way", ["sd", "nfc", "qr", "vdisk"])
|
|
def test_wif_store_import(num_wifs, separator, way, import_wif_to_store, skip_if_useless_way,
|
|
settings_remove, goto_home):
|
|
skip_if_useless_way(way)
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [make_fake_wif() for _ in range(num_wifs)]
|
|
|
|
import_wif_to_store(wif_list, way=way, sep=separator)
|
|
goto_home()
|
|
|
|
|
|
def test_wif_store_import_manual(import_wif_to_store, settings_remove, goto_home):
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [make_fake_wif()]
|
|
|
|
import_wif_to_store(wif_list, way="input")
|
|
goto_home()
|
|
|
|
|
|
def test_wif_store_import_paper_wallet(goto_home, pick_menu_item, press_select, cap_story,
|
|
need_keypress, settings_remove, microsd_path, cap_menu):
|
|
settings_remove("wifs")
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
try:
|
|
pick_menu_item('Paper Wallets')
|
|
except:
|
|
raise pytest.skip('Feature absent')
|
|
|
|
press_select()
|
|
pick_menu_item('GENERATE WALLET')
|
|
|
|
time.sleep(0.1)
|
|
title, story = cap_story()
|
|
if "Press (1) to save paper wallet file to SD Card" in story:
|
|
need_keypress("1")
|
|
time.sleep(0.2)
|
|
title, story = cap_story()
|
|
assert 'Created file' in story
|
|
|
|
story = [i for i in story.split('\n') if i]
|
|
fname = story[-2]
|
|
assert fname.endswith('.txt')
|
|
|
|
with open(microsd_path(fname), "r") as f:
|
|
const = f.read()
|
|
|
|
goto_home()
|
|
pick_menu_item('Advanced/Tools')
|
|
pick_menu_item("WIF Store")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
if title == "WIF Store":
|
|
press_select()
|
|
pick_menu_item("Import WIF")
|
|
need_keypress("1") # SD
|
|
try:
|
|
pick_menu_item(fname)
|
|
except:
|
|
pass
|
|
|
|
menu = cap_menu()
|
|
assert "Import WIF" in menu
|
|
assert len(menu) == 2 # only one WIF imported from paper wallet that contins 2x same WIF
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Detail")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert story.split("\n\n")[0] == const.split("\n\n")[4].strip()
|
|
|
|
|
|
@pytest.mark.parametrize("wif,err,way", [
|
|
("Ky2BtsR8qRN91PjktxaTQWMgJZUWSBJLjwip642vvoNyH1PeEpUP", "chain", "qr"), # mainnet key on testnet
|
|
("91zb4oYGEvwEroihAbkdeoBpLSKnZYMdD1CPhfQD76fxrfNSp5J", "compressed only", "sd"), # uncompressed pk
|
|
("cWALDjUu1tszsCBMjBjL4mhYj2wHUWYDR8Q8aSjLKzjkWaXMLRaY", None, "sd"), # curve order
|
|
("cMahea7zqjxrtgAbB7LSGbcQUr1uX1ojuat9jZodMN87J7g8rY9t", None, "nfc"), # zero
|
|
("cPPBMnQzGV4QAqD2HNPamprjvnmv6dQ2oysHCUVSRv2yXkVvWVtX", None, "nfc"), # wrong csum
|
|
("cPPBMnQzGV4QAqD2HNPamprjvnmv6dQ2oysHCUVSRv2yXkVvWVtX;cN7M6sNzn4LGBxAozsmphxjuxVNaHcLre7Nm163qM3DpY3BZog1v", None, "sd"), # wrong separator
|
|
])
|
|
def test_wif_store_import_fail(way, wif, err, import_wif_to_store, skip_if_useless_way,
|
|
settings_remove, press_select, cap_story, use_testnet, settings_get):
|
|
|
|
err = err or "No valid WIF key found"
|
|
skip_if_useless_way(way)
|
|
use_testnet()
|
|
settings_remove("wifs")
|
|
|
|
import_wif_to_store([wif], way=way, early_exit=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Failed to import WIF" in story
|
|
assert err in story
|
|
press_select()
|
|
assert not settings_get("wifs")
|
|
|
|
|
|
@pytest.mark.parametrize("netcode", ["XTN", "BTC"])
|
|
def test_wif_store_detail(netcode, import_wif_to_store, use_mainnet, cap_menu, pick_menu_item,
|
|
cap_story, need_keypress, settings_remove, cap_screen_qr, is_q1,
|
|
press_cancel, nfc_is_enabled, press_nfc, nfc_read_text, goto_home):
|
|
goto_home()
|
|
if netcode == "BTC":
|
|
use_mainnet()
|
|
|
|
settings_remove("wifs")
|
|
|
|
prefix = bytes([128]) if netcode == "BTC" else bytes([239])
|
|
privkeys = [PrivateKey.parse(prandom(32)) for _ in range(5)]
|
|
wif_list = [encode_base58_checksum(prefix + bytes(sk) + b'\x01') for sk in privkeys]
|
|
|
|
import_wif_to_store(wif_list)
|
|
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
target_mi = []
|
|
for mi in menu:
|
|
if "⋯" in mi:
|
|
target_mi.append(mi)
|
|
|
|
assert len(target_mi) == len(wif_list)
|
|
for mi, wif, sk in zip(target_mi, wif_list, privkeys):
|
|
mi_split = mi.split(" ")[-1].split("⋯")
|
|
assert len(mi_split) == 2
|
|
assert mi_split[0] in wif
|
|
assert mi_split[1] in wif
|
|
pick_menu_item(mi)
|
|
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
assert menu[0] == "Detail"
|
|
assert menu[1] == "Descriptors"
|
|
assert menu[2] == "Addresses"
|
|
assert menu[3] == "Sign MSG"
|
|
assert menu[4] == "Delete"
|
|
|
|
pick_menu_item("Detail")
|
|
|
|
title, story = cap_story()
|
|
assert title == "WIF"
|
|
|
|
split_story = story.split("\n\n")
|
|
story_wif = split_story[0]
|
|
story_sk = split_story[1].split("\n")[-1]
|
|
story_pk = split_story[2].split("\n")[-1]
|
|
|
|
assert f'{KEY_QR if is_q1 else "(4)"} to show QR code' in story
|
|
|
|
need_keypress(KEY_QR if is_q1 else "4")
|
|
time.sleep(.1)
|
|
wif_qr = cap_screen_qr().decode()
|
|
press_cancel()
|
|
|
|
if nfc_is_enabled():
|
|
assert f"{KEY_NFC if is_q1 else '(3)'} to share via NFC" in story
|
|
|
|
press_nfc()
|
|
time.sleep(0.3)
|
|
nfc_wif = nfc_read_text()
|
|
time.sleep(0.3)
|
|
press_cancel()
|
|
assert nfc_wif == wif
|
|
|
|
|
|
assert story_wif == wif == wif_qr
|
|
assert story_sk == bytes(sk).hex()
|
|
assert story_pk == sk.K.sec().hex()
|
|
|
|
press_cancel() # exit Detail
|
|
press_cancel() # exit WIF submenu
|
|
|
|
|
|
@pytest.mark.parametrize("netcode", ["XTN", "BTC"])
|
|
def test_wif_store_addresses(netcode, import_wif_to_store, use_mainnet, cap_menu, pick_menu_item,
|
|
cap_story, need_keypress, settings_remove, cap_screen_qr, is_q1,
|
|
nfc_is_enabled, press_nfc, nfc_read_text, goto_home, press_cancel):
|
|
goto_home()
|
|
if netcode == "BTC":
|
|
use_mainnet()
|
|
|
|
settings_remove("wifs")
|
|
|
|
prefix = bytes([128]) if netcode == "BTC" else bytes([239])
|
|
n = BIP32Node.from_master_secret(prandom(32))
|
|
privkey = n.node.private_key
|
|
|
|
wif_list = [ encode_base58_checksum(prefix + bytes(privkey) + b'\x01') ]
|
|
|
|
import_wif_to_store(wif_list)
|
|
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Addresses")
|
|
|
|
for mi, af in [("P2SH-Segwit", "p2sh-p2wpkh"), ("Segwit P2WPKH", "p2wpkh"), ("Classic P2PKH", "p2pkh")]:
|
|
pick_menu_item(mi)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
if is_q1:
|
|
# Q has title as it needs hint keys
|
|
assert title == mi
|
|
|
|
target_addr = n.address(addr_fmt=af, netcode=netcode)
|
|
addr = addr_from_display_format(story.split("\n\n")[0])
|
|
assert addr == target_addr
|
|
|
|
if not is_q1:
|
|
assert "(4) to show QR code" in story
|
|
|
|
need_keypress(KEY_QR if is_q1 else "4")
|
|
time.sleep(.1)
|
|
qr_addr = cap_screen_qr().decode()
|
|
if af == "p2wpkh":
|
|
qr_addr = qr_addr.lower()
|
|
press_cancel()
|
|
assert qr_addr == target_addr
|
|
|
|
if nfc_is_enabled():
|
|
if not is_q1:
|
|
assert "(3) to share via NFC" in story
|
|
|
|
press_nfc()
|
|
time.sleep(0.3)
|
|
nfc_addr = nfc_read_text()
|
|
time.sleep(0.3)
|
|
press_cancel()
|
|
assert nfc_addr == target_addr
|
|
|
|
press_cancel()
|
|
press_cancel()
|
|
press_cancel()
|
|
|
|
|
|
def test_wif_store_clear_all(import_wif_to_store, press_select, cap_story, settings_get,
|
|
need_keypress, cap_menu, settings_remove, is_q1, goto_home):
|
|
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
wif_list = [make_fake_wif() for _ in range(30)] # 30 is the max
|
|
import_wif_to_store(wif_list)
|
|
time.sleep(.1)
|
|
|
|
menu = cap_menu()
|
|
assert "Import WIF" not in menu # WIF store is full
|
|
assert "Clear All" in menu
|
|
need_keypress(KEY_UP if is_q1 else "5")
|
|
time.sleep(.1)
|
|
press_select() # on Clear All
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Remove all saved WIF keys?" in story
|
|
assert "(4)" in story
|
|
press_select() # does not work & gets you back to menu
|
|
assert len(settings_get("wifs")) == 30
|
|
|
|
press_select() # on Clear All
|
|
time.sleep(.1)
|
|
need_keypress("4")
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
assert "(none yet)" in menu
|
|
assert "Import WIF" in menu
|
|
assert not settings_get("wifs")
|
|
|
|
|
|
def test_wif_store_capacity(import_wif_to_store, settings_remove, press_select, cap_story,
|
|
settings_get, cap_menu, pick_menu_item, need_keypress):
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [make_fake_wif() for _ in range(40)] # MAX+1
|
|
|
|
import_wif_to_store(wif_list[:31], early_exit=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "Failure"
|
|
assert "Max 30 items allowed in WIF Store" in story
|
|
assert "Attempted to import 31 keys" in story
|
|
assert "remaining WIF store capacity is only 30"
|
|
press_select()
|
|
assert not settings_get("wifs")
|
|
|
|
# import 29 keys
|
|
import_wif_to_store(wif_list[:29])
|
|
|
|
assert len(settings_get("wifs", [])) == 29
|
|
|
|
import_wif_to_store(wif_list[-2:], early_exit=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "Failure"
|
|
assert "Max 30 items allowed in WIF Store" in story
|
|
assert "Attempted to import 2 keys" in story
|
|
assert "remaining WIF store capacity is only 1"
|
|
press_select()
|
|
|
|
assert len(settings_get("wifs", [])) == 29
|
|
|
|
import_wif_to_store(wif_list[-1:])
|
|
assert len(settings_get("wifs", [])) == 30
|
|
|
|
menu = cap_menu()
|
|
assert "Import WIF" not in menu
|
|
# remove random key to make space
|
|
# pick key at current menu item position
|
|
press_select()
|
|
time.sleep(.1)
|
|
pick_menu_item("Delete")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "Delete WIF key?" in story
|
|
press_select()
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
assert "Import WIF" in menu
|
|
|
|
|
|
def test_visualize_wif_store_capacity(is_q1, goto_home, use_testnet, settings_remove,
|
|
import_wif_to_store, settings_get, need_keypress,
|
|
scan_a_qr, cap_story, press_select):
|
|
if not is_q1:
|
|
raise pytest.skip("need scanner")
|
|
|
|
settings_remove("wifs")
|
|
use_testnet()
|
|
|
|
goto_home()
|
|
import_wif_to_store([make_fake_wif() for _ in range(30)])
|
|
assert len(settings_get("wifs", [])) == 30
|
|
|
|
goto_home()
|
|
need_keypress(KEY_QR)
|
|
scan_a_qr("cUR6JLQCmdPPt3op4jEYmFhjHpWC2AoZaWmZqoDaBQYMXN4QeKuc")
|
|
time.sleep(1)
|
|
|
|
title, story = cap_story()
|
|
assert title == "WIF Key"
|
|
assert "Press (1) to import to WIF Store" in story
|
|
|
|
need_keypress("1")
|
|
time.sleep(.1)
|
|
|
|
title, story = cap_story()
|
|
assert title == "Failure"
|
|
assert "Max 30 items allowed in WIF Store" in story
|
|
assert len(settings_get("wifs", [])) == 30
|
|
press_select()
|
|
|
|
|
|
def test_wif_store_import_duplicate(settings_remove, import_wif_to_store, settings_get, cap_menu, cap_story,
|
|
goto_home):
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [make_fake_wif() for _ in range(4)]
|
|
|
|
import_wif_to_store(wif_list)
|
|
b4 = cap_menu()
|
|
assert len(settings_get("wifs")) == 4
|
|
|
|
import_wif_to_store(wif_list, early_exit=True)
|
|
assert len(settings_get("wifs")) == 4
|
|
assert len(b4) == len(cap_menu())
|
|
|
|
title, story = cap_story()
|
|
assert 'duplicate WIF' in story
|
|
|
|
|
|
@pytest.mark.parametrize("way", ["qr", "sd", "nfc"])
|
|
def test_wif_store_export_all(way, goto_home, settings_remove, import_wif_to_store, pick_menu_item,
|
|
load_export, press_cancel):
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [make_fake_wif() for _ in range(6)] # 6*52 chars so it can be shown on mk4 too
|
|
|
|
import_wif_to_store(wif_list)
|
|
time.sleep(.1)
|
|
pick_menu_item("Export All")
|
|
conts = load_export(way, "WIF Store", is_json=False, sig_check=False)
|
|
|
|
assert wif_list == conts.split("\n")
|
|
press_cancel()
|
|
|
|
|
|
@pytest.mark.parametrize('en_okeys', [ True, False])
|
|
def test_hobbled_wif_store(en_okeys, set_hobble, settings_remove, import_wif_to_store, goto_home,
|
|
cap_menu, pick_menu_item):
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
|
|
wif_list = [
|
|
encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
|
|
for _ in range(3)
|
|
]
|
|
|
|
import_wif_to_store(wif_list)
|
|
goto_home()
|
|
|
|
set_hobble(True, {'okeys'} if en_okeys else {})
|
|
pick_menu_item("Advanced/Tools")
|
|
|
|
if en_okeys:
|
|
pick_menu_item("WIF Store")
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
# check it is read-only
|
|
assert "Import WIF" not in menu
|
|
assert "Clear All" not in menu
|
|
pick_menu_item(menu[0])
|
|
time.sleep(.1)
|
|
menu = cap_menu()
|
|
assert "Delete" not in menu
|
|
else:
|
|
assert "WIF Store" not in cap_menu()
|
|
|
|
|
|
@pytest.mark.parametrize("way,af", [
|
|
("sd", "P2SH-Segwit"),
|
|
("input", "Segwit P2WPKH"),
|
|
("nfc", "Classic P2PKH")
|
|
])
|
|
def test_sign_msg_with_wif_store_key(way, af, settings_remove, import_wif_to_store, cap_menu,
|
|
pick_menu_item, cap_story, need_keypress, press_nfc,
|
|
enter_complex, garbage_collector, microsd_path, nfc_write_text,
|
|
verify_msg_sign_story, msg_sign_export, press_select, goto_home):
|
|
settings_remove("wifs")
|
|
msg = "Coinkite"
|
|
|
|
n = BIP32Node.from_master_secret(os.urandom(32))
|
|
privkey = n.node.private_key
|
|
import_wif_to_store([encode_base58_checksum(bytes([239]) + bytes(privkey) + b'\x01')])
|
|
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Sign MSG")
|
|
pick_menu_item(af)
|
|
|
|
if way == "input":
|
|
need_keypress("0")
|
|
enter_complex(msg, apply=False, b39pass=False)
|
|
|
|
elif way == "sd":
|
|
name = "msg_to_sign.txt"
|
|
pth = microsd_path(name)
|
|
with open(pth, "w") as f:
|
|
f.write(msg)
|
|
|
|
need_keypress("1")
|
|
pick_menu_item(name)
|
|
|
|
elif way == "nfc":
|
|
press_nfc()
|
|
time.sleep(0.2)
|
|
nfc_write_text(msg)
|
|
time.sleep(0.3)
|
|
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
addr_fmt = {"P2SH-Segwit": "p2sh-p2wpkh",
|
|
"Segwit P2WPKH": "p2wpkh",
|
|
"Classic P2PKH": "p2pkh"}[af]
|
|
|
|
target_addr = n.address(addr_fmt=addr_fmt)
|
|
verify_msg_sign_story(story, msg, "m", addr=target_addr)
|
|
press_select()
|
|
res = msg_sign_export(way if way != "input" else "sd")
|
|
assert target_addr in res
|
|
pmsg, addr, sig = parse_signed_message(res)
|
|
assert pmsg == msg
|
|
assert verify_message(addr, sig, msg) is True
|
|
goto_home()
|
|
|
|
|
|
@pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh", "p2sh"])
|
|
def test_multisig_wif_store(addr_fmt, dev, fake_ms_txn, start_sign, settings_set, clear_ms,
|
|
cap_story, pytestconfig, import_ms_wallet, end_sign, settings_remove):
|
|
# TODO This test MUST be run with --psbt2 flag on and off
|
|
clear_ms()
|
|
settings_remove("wifs")
|
|
M, N = 3, 5
|
|
|
|
if addr_fmt == AF_P2SH:
|
|
dd = "m/45h"
|
|
elif addr_fmt == AF_P2WSH:
|
|
dd = "m/48h/1h/0h/2h"
|
|
else:
|
|
dd = "m/48h/1h/0h/1h"
|
|
|
|
def path_mapper(idx):
|
|
kk = str_to_path(dd)
|
|
return kk + [0,0]
|
|
|
|
keys = import_ms_wallet(M, N, name='wif_store', accept=True, netcode="XTN",
|
|
descriptor=True, addr_fmt=addr_fmt, common=dd)
|
|
|
|
psbt = fake_ms_txn(1, 1, M, keys, inp_af=unmap_addr_fmt[addr_fmt],
|
|
path_mapper=path_mapper, psbt_v2=pytestconfig.getoption('psbt2'))
|
|
|
|
# sign with master key first - nothing in WIF store
|
|
# without warning
|
|
# one signature from master added
|
|
start_sign(psbt)
|
|
title, story = cap_story()
|
|
assert "warning" not in story
|
|
signed = end_sign()
|
|
|
|
po = BasicPSBT().parse(signed)
|
|
assert len(po.inputs[0].part_sigs) == 1
|
|
|
|
# add privkey from 0th & 1st node to WIF store
|
|
der_node0 = keys[0][1].subkey_for_path(dd[2:] + "/0/0")
|
|
sk0 = bytes(der_node0.node.private_key).hex()
|
|
pk0 = der_node0.node.private_key.K.sec().hex()
|
|
der_node1 = keys[1][1].subkey_for_path(dd[2:] + "/0/0")
|
|
sk1 = bytes(der_node1.node.private_key).hex()
|
|
pk1 = der_node1.node.private_key.K.sec().hex()
|
|
settings_set("wifs", [(pk0,sk0), (pk1,sk1)])
|
|
|
|
# ofe of the private keys will be used for signing
|
|
# only one as we cannot sign with 2 keys in one sitting
|
|
start_sign(signed)
|
|
title, story = cap_story()
|
|
assert "warning" in story
|
|
assert "WIF store" in story
|
|
signed = end_sign()
|
|
po = BasicPSBT().parse(signed)
|
|
assert len(po.inputs[0].part_sigs) == 2
|
|
|
|
# sign with other key - keys that already have signatures are ignored
|
|
# that is why we can proceed with this iterative method
|
|
start_sign(signed, finalize=True)
|
|
title, story = cap_story()
|
|
assert "warning" in story
|
|
assert "WIF store" in story
|
|
end_sign(finalize=True)
|
|
|
|
|
|
@pytest.mark.parametrize("addr_fmt", ["p2wpkh", "p2sh-p2wpkh", "p2pkh"])
|
|
@pytest.mark.parametrize("idx", [1, 3])
|
|
def test_wif_store_ownership(addr_fmt, idx, is_q1, goto_home, pick_menu_item, scan_a_qr, cap_story,
|
|
need_keypress, src_root_dir, sim_root_dir, nfc_write, settings_remove,
|
|
import_wif_to_store, load_shared_mod, cap_screen_qr, press_cancel):
|
|
|
|
settings_remove("wifs")
|
|
|
|
n = BIP32Node.from_master_secret(os.urandom(32))
|
|
privkey = n.node.private_key
|
|
addr = n.address(addr_fmt=addr_fmt)
|
|
wif = encode_base58_checksum(bytes([239]) + bytes(privkey) + b'\x01')
|
|
wif1 = encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
|
|
wif2 = encode_base58_checksum(bytes([239]) + os.urandom(32) + b'\x01')
|
|
|
|
if idx == 1:
|
|
wif_list = [wif, wif1, wif2]
|
|
else:
|
|
wif_list = [wif1, wif2, wif]
|
|
|
|
import_wif_to_store(wif_list)
|
|
|
|
goto_home()
|
|
|
|
if is_q1:
|
|
pick_menu_item('Scan Any QR Code')
|
|
scan_a_qr(addr)
|
|
time.sleep(1)
|
|
|
|
title, story = cap_story()
|
|
|
|
assert addr == addr_from_display_format(story.split("\n\n")[0])
|
|
assert '(1) to verify ownership' in story
|
|
need_keypress('1')
|
|
|
|
else:
|
|
cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py')
|
|
n = cc_ndef.ndefMaker()
|
|
n.add_text(addr)
|
|
ccfile = n.bytes()
|
|
|
|
pick_menu_item('Advanced/Tools')
|
|
pick_menu_item('NFC Tools')
|
|
pick_menu_item('Verify Address')
|
|
with open(f'{sim_root_dir}/debug/nfc-addr.ndef', 'wb') as f:
|
|
f.write(ccfile)
|
|
nfc_write(ccfile)
|
|
|
|
time.sleep(1)
|
|
title, story = cap_story()
|
|
assert addr == addr_from_display_format(story.split("\n\n")[0])
|
|
assert f"Found in WIF store at index {idx}" in story
|
|
need_keypress(KEY_QR if is_q1 else '1')
|
|
addr_qr = cap_screen_qr().decode()
|
|
if addr_fmt == "p2wpkh":
|
|
addr_qr = addr_qr.lower()
|
|
|
|
assert addr == addr_qr
|
|
press_cancel()
|
|
|
|
|
|
@pytest.mark.parametrize("num_ins", [1, 5])
|
|
@pytest.mark.parametrize("addr_fmt", ["p2pkh", "p2wpkh", "p2sh-p2wpkh"])
|
|
def test_wif_store_signing(num_ins, addr_fmt, fake_txn, goto_home, pick_menu_item, need_keypress,
|
|
start_sign, end_sign, cap_menu, cap_story, press_cancel, settings_remove,
|
|
press_select, import_wif_to_store):
|
|
|
|
settings_remove("wifs")
|
|
|
|
wrap = False
|
|
if addr_fmt == "p2pkh":
|
|
sw = False
|
|
elif addr_fmt == "p2wpkh":
|
|
sw = True
|
|
elif addr_fmt == "p2sh-p2wpkh":
|
|
wrap = True
|
|
sw = True
|
|
else:
|
|
raise ValueError
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(num_ins, 1, segwit_in=sw, wrapped=wrap, master_xpub=node.hwif())
|
|
|
|
wifs = []
|
|
for i in range(num_ins):
|
|
n = node.subkey_for_path("0/%d" % i)
|
|
wifs.append(n.node.private_key.wif(testnet=True))
|
|
|
|
import_wif_to_store(wifs)
|
|
|
|
menu = cap_menu()
|
|
assert menu[0] == "Import WIF"
|
|
|
|
start_sign(psbt, finalize=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "warning" in story
|
|
if num_ins == 1:
|
|
assert "WIF store: 0" in story
|
|
else:
|
|
assert f"WIF store: {', '.join([str(i) for i in range(num_ins)])}" in story
|
|
end_sign(finalize=True)
|
|
|
|
|
|
@pytest.mark.parametrize("der_paths", [True, False])
|
|
@pytest.mark.parametrize("complete", [True, False])
|
|
def test_wif_store_signing_multi(der_paths, complete, fake_txn, start_sign, end_sign, cap_story,
|
|
settings_set):
|
|
wifs = []
|
|
|
|
hack = None
|
|
if der_paths:
|
|
def hack(psbt):
|
|
new_paths = {}
|
|
for k, v in psbt.inputs[0].bip32_paths.items():
|
|
new_paths[k] = b"\x01" * 8 # garbage (do not use zero xfp here)
|
|
|
|
psbt.inputs[0].bip32_paths = new_paths
|
|
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=True, master_xpub=node.hwif(), psbt_v2=True, outvals=[1E8*3],
|
|
psbt_hacker=hack)
|
|
po = BasicPSBT().parse(psbt)
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
wifs.append((pk, sk))
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=False, master_xpub=node.hwif(), psbt_v2=True, psbt_hacker=hack)
|
|
tmp = BasicPSBT().parse(psbt)
|
|
po.inputs += tmp.inputs
|
|
po.input_count += 1
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
wifs.append((pk, sk))
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=True, wrapped=True, master_xpub=node.hwif(), psbt_v2=True,
|
|
psbt_hacker=hack)
|
|
tmp = BasicPSBT().parse(psbt)
|
|
po.inputs += tmp.inputs
|
|
po.input_count += 1
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
wifs.append((pk, sk))
|
|
|
|
# pretend we have those imported
|
|
if not complete:
|
|
wifs = wifs[:-1]
|
|
|
|
settings_set("wifs", wifs)
|
|
|
|
start_sign(po.as_bytes(), finalize=complete)
|
|
title, story = cap_story()
|
|
assert "warning" in story
|
|
if complete:
|
|
assert "WIF store: 0, 1, 2" in story
|
|
else:
|
|
assert "WIF store: 0, 1" in story
|
|
assert "Limited Signing" in story
|
|
|
|
end_sign(finalize=complete)
|
|
|
|
|
|
def test_wif_store_signing_with_master(fake_txn, start_sign, end_sign, cap_story, settings_set):
|
|
# signs both master key and keys from WIF store
|
|
wifs = []
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=True, master_xpub=node.hwif(), psbt_v2=True, outvals=[1E8*3])
|
|
po = BasicPSBT().parse(psbt)
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
wifs.append((pk, sk))
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=False, master_xpub=node.hwif(), psbt_v2=True)
|
|
tmp = BasicPSBT().parse(psbt)
|
|
po.inputs += tmp.inputs
|
|
po.input_count += 1
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
wifs.append((pk, sk))
|
|
|
|
# add simulator input
|
|
psbt = fake_txn(1, 1, segwit_in=True, psbt_v2=True)
|
|
tmp = BasicPSBT().parse(psbt)
|
|
po.inputs += tmp.inputs
|
|
po.input_count += 1
|
|
|
|
|
|
settings_set("wifs", wifs)
|
|
|
|
# convert to v0 PSBT just for fun
|
|
start_sign(po.to_v0(), finalize=True)
|
|
title, story = cap_story()
|
|
assert "warning" in story
|
|
assert "WIF store: 0, 1" in story
|
|
|
|
end_sign(finalize=True)
|
|
|
|
@pytest.mark.parametrize("wif", [
|
|
"KwYP78wzyiuShCqppuh1JZQCnKtFdAaY6HcDhRmhDy21vGSiF37N", # mainnet compressed
|
|
"5JwcuSWKH4PqV1mU8JSK9BBUkLjuAUS3MFHfP1w1qy9HjnXpavk", # mainnet uncompressed
|
|
"91cLPdroy4CtRYxWBXxgggqNnZrTz2CoJrLDkjDjcnkMP74gX5S", # testnet uncompressed
|
|
"cUR6JLQCmdPPt3op4jEYmFhjHpWC2AoZaWmZqoDaBQYMXN4QeKuc", # testnet compressed
|
|
])
|
|
@pytest.mark.parametrize("testnet", [True, False])
|
|
def test_visualize_wif(wif, testnet, is_q1, goto_home, need_keypress, use_testnet, use_mainnet,
|
|
scan_a_qr, cap_story, settings_remove, press_select):
|
|
if not is_q1:
|
|
raise pytest.skip("need scanner")
|
|
|
|
settings_remove("wifs")
|
|
|
|
if testnet:
|
|
use_testnet()
|
|
else:
|
|
use_mainnet()
|
|
|
|
goto_home()
|
|
need_keypress(KEY_QR)
|
|
scan_a_qr(wif)
|
|
time.sleep(1)
|
|
title, story = cap_story()
|
|
split_story = story.split("\n\n")
|
|
pubkey = split_story[3].split("\n")[-1]
|
|
if wif[0] in "59":
|
|
# uncompressed
|
|
assert pubkey[0:2] == "04"
|
|
assert len(pubkey) == 130
|
|
else:
|
|
# compressed
|
|
assert pubkey[0:2] in ["02", "03"]
|
|
assert len(pubkey) == 66
|
|
|
|
if testnet:
|
|
# we are on testnet, mainnet keys are not importable
|
|
if wif[0] in "K59":
|
|
assert "Press (1) to import to WIF Store" not in story
|
|
return
|
|
else:
|
|
# we are on mainnet, testnet keys are not importable
|
|
if wif[0] in "c59":
|
|
assert "Press (1) to import to WIF Store" not in story
|
|
return
|
|
|
|
assert "Press (1) to import to WIF Store" in story
|
|
need_keypress("1")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "Success"
|
|
assert "Saved to WIF Store" in story
|
|
press_select()
|
|
|
|
# try import same wif
|
|
goto_home()
|
|
need_keypress(KEY_QR)
|
|
scan_a_qr(wif)
|
|
time.sleep(1)
|
|
need_keypress("1")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "Failure"
|
|
assert "duplicate WIF" in story
|
|
press_select()
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
def test_descriptor_export(import_wif_to_store, cap_menu, goto_home, settings_remove,
|
|
pick_menu_item, skip_if_useless_way, need_keypress, load_export,
|
|
cap_story, is_q1, bitcoind, press_cancel):
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
pk = node.node.private_key
|
|
wif_str = pk.wif(testnet=True)
|
|
|
|
target = f"wpkh({node.node.public_key.sec().hex()})"
|
|
target = bitcoind.rpc.getdescriptorinfo(target)["descriptor"]
|
|
|
|
import_wif_to_store([wif_str])
|
|
# now in wif store menu, only one menu item besides "Import WIF"
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("Segwit P2WPKH")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
story_desc = story.split("\n\n")[0]
|
|
assert story_desc.strip() == target
|
|
|
|
need_keypress("1") # SD
|
|
sd_desc = load_export("sd", "Descriptor", is_json=False, sig_check=False)
|
|
assert sd_desc.strip() == target
|
|
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
if "QR" in story:
|
|
qr_desc = load_export("qr", "Descriptor", is_json=False, sig_check=False)
|
|
press_cancel() # exit QR disaply
|
|
assert qr_desc.strip() == target
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
|
|
if "NFC" in story:
|
|
nfc_desc = load_export("nfc", "Descriptor", is_json=False, sig_check=False)
|
|
assert nfc_desc.strip() == target
|
|
press_cancel()
|
|
|
|
goto_home()
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize('mode', [ "Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH"])
|
|
def test_spend_paper_wallet_desc_core(mode, bitcoind, settings_remove, import_wif_to_store,
|
|
start_sign, end_sign, cap_story, use_regtest, cap_menu,
|
|
pick_menu_item, goto_home, need_keypress, load_export):
|
|
use_regtest()
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
amount = 5 # BTC
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
pk = node.node.private_key
|
|
wif_str = pk.wif(testnet=True)
|
|
|
|
import_wif_to_store([wif_str])
|
|
# now in wif store menu, only one menu item besides "Import WIF"
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item(mode)
|
|
need_keypress("1") # SD
|
|
desc = load_export("sd", "Descriptor", is_json=False, sig_check=False)
|
|
|
|
# must match pubkey from device
|
|
assert pk.K.sec().hex() in desc
|
|
|
|
paper_addr = bitcoind.rpc.deriveaddresses(desc)[0]
|
|
|
|
paper = bitcoind.create_wallet(wallet_name="paper-wif", disable_private_keys=True,
|
|
blank=True, descriptors=True)
|
|
res = paper.importdescriptors([{
|
|
"desc": desc, "timestamp": 0, "watchonly": True,
|
|
}])
|
|
assert len(res) == 1 and res[0]["success"]
|
|
|
|
bitcoind.supply_wallet.sendtoaddress(paper_addr, amount)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
assert paper.listunspent()
|
|
|
|
dest = bitcoind.supply_wallet.getnewaddress()
|
|
resp = paper.walletcreatefundedpsbt([], [{dest: amount}], 0,
|
|
{"fee_rate": 3, "subtractFeeFromOutputs": [0]})
|
|
|
|
po = BasicPSBT().parse(base64.b64decode(resp["psbt"]))
|
|
# first sign as provided by core
|
|
psbt_bytes = po.as_bytes()
|
|
start_sign(psbt_bytes, finalize=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "WIF store: 0" in story
|
|
signed = end_sign(accept=True, finalize=True)
|
|
|
|
# remove BIP-32 paths from PSBT inputs
|
|
# causes auto-detection on CC side
|
|
for i in range(len(po.inputs)):
|
|
po.inputs[i].bip32_paths = None
|
|
|
|
psbt1_bytes = po.as_bytes()
|
|
assert len(psbt_bytes) > len(psbt1_bytes)
|
|
start_sign(psbt1_bytes, finalize=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "WIF store: 0" in story
|
|
signed1 = end_sign(accept=True, finalize=True)
|
|
assert signed1 == signed
|
|
|
|
tx_hex = signed.hex()
|
|
accept = bitcoind.rpc.testmempoolaccept([tx_hex])
|
|
|
|
assert accept[0]["allowed"]
|
|
txid = bitcoind.rpc.sendrawtransaction(tx_hex)
|
|
assert len(txid) == 64
|
|
|
|
settings_remove("wifs")
|
|
goto_home()
|
|
|
|
|
|
@pytest.mark.parametrize("wif_store", [True, False])
|
|
@pytest.mark.parametrize("subpaths", [True, False])
|
|
def test_no_keys(wif_store, subpaths, fake_txn, settings_set, start_sign,
|
|
cap_story, settings_remove):
|
|
|
|
hack = None
|
|
if subpaths is False:
|
|
def hack(psbt):
|
|
psbt.inputs[0].bip32_paths = None
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(1, 1, segwit_in=True, master_xpub=node.hwif(), psbt_v2=True, psbt_hacker=hack)
|
|
|
|
# overwrite node, causing PSBT to be from completely different WIF/address
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
n = node.subkey_for_path("0/0")
|
|
sk = bytes(n.node.private_key).hex()
|
|
pk = n.node.private_key.K.sec().hex()
|
|
|
|
if wif_store:
|
|
settings_set("wifs", [(pk, sk)])
|
|
else:
|
|
settings_remove("wifs")
|
|
|
|
start_sign(psbt, finalize=True)
|
|
title, story = cap_story()
|
|
assert "Failure" == title
|
|
|
|
if wif_store is False and subpaths is False:
|
|
assert "PSBT does not contain any key path information" in story
|
|
else:
|
|
assert "None of the keys involved in this transaction belong to this Coldcard" in story
|
|
|
|
|
|
def test_unrelated_wif_does_not_allow_presigned_foreign_psbt(fake_txn, settings_set,
|
|
start_sign, cap_story):
|
|
foreign = BIP32Node.from_master_secret(os.urandom(32))
|
|
psbt = fake_txn(2, 1, segwit_in=True, master_xpub=foreign.hwif(), psbt_v2=True)
|
|
po = BasicPSBT().parse(psbt)
|
|
|
|
pubkey = list(po.inputs[0].bip32_paths.keys())[0]
|
|
po.inputs[0].part_sigs[pubkey] = b'\x30' + os.urandom(70)
|
|
|
|
unrelated = BIP32Node.from_master_secret(os.urandom(32)).subkey_for_path("0/0")
|
|
sk = bytes(unrelated.node.private_key).hex()
|
|
pk = unrelated.node.private_key.K.sec().hex()
|
|
settings_set("wifs", [(pk, sk)])
|
|
|
|
start_sign(po.as_bytes(), finalize=False)
|
|
title, story = cap_story()
|
|
assert title == "Failure"
|
|
assert "None of the keys involved in this transaction belong to this Coldcard" in story
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize('mode', ["Classic P2PKH", "Segwit P2WPKH", "P2SH-Segwit"])
|
|
def test_spend_paper_wallet_addr_only(mode, bitcoind, settings_remove, import_wif_to_store,
|
|
start_sign, end_sign, cap_story, use_regtest,
|
|
pick_menu_item, goto_home, cap_menu, need_keypress,
|
|
load_export):
|
|
use_regtest()
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
amount = 10 # BTC
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
pk = node.node.private_key
|
|
wif_str = pk.wif(testnet=True)
|
|
|
|
import_wif_to_store([wif_str])
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
|
|
# Use the device-exported descriptor only to derive the address; we'll
|
|
# build the watch-only wallet around addr() instead of pkh/wpkh.
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item(mode)
|
|
need_keypress("1") # SD
|
|
desc = load_export("sd", "Descriptor", is_json=False, sig_check=False)
|
|
paper_addr = bitcoind.rpc.deriveaddresses(desc.strip())[0]
|
|
|
|
# Watch-only wallet built from addr() — no pubkey knowledge at all.
|
|
addr_desc = bitcoind.rpc.getdescriptorinfo("addr(%s)" % paper_addr)["descriptor"]
|
|
|
|
wname = "paper-addr-%s" % mode.replace(' ', '-')
|
|
paper = bitcoind.create_wallet(wallet_name=wname, disable_private_keys=True,
|
|
blank=True, descriptors=True)
|
|
res = paper.importdescriptors([{
|
|
"desc": addr_desc, "timestamp": "now", "watchonly": True,
|
|
}])
|
|
assert len(res) == 1 and res[0]["success"], res
|
|
|
|
# two inputs
|
|
bitcoind.supply_wallet.sendtoaddress(paper_addr, amount/2)
|
|
bitcoind.supply_wallet.sendtoaddress(paper_addr, amount/2)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
assert paper.listunspent()
|
|
|
|
dest = bitcoind.supply_wallet.getnewaddress()
|
|
# solving_data lets Core estimate the dummy signature size for fee
|
|
# selection; it is NOT written into the PSBT, so bip32_paths stays empty.
|
|
pubkey_hex = node.node.public_key.sec().hex()
|
|
resp = paper.walletcreatefundedpsbt(
|
|
[], [{dest: amount}], 0,
|
|
{"fee_rate": 3, "subtractFeeFromOutputs": [0],
|
|
"solving_data": {"pubkeys": [pubkey_hex]}})
|
|
psbt_bytes = base64.b64decode(resp["psbt"])
|
|
|
|
# Sanity check
|
|
po = BasicPSBT().parse(psbt_bytes)
|
|
for i, inp in enumerate(po.inputs):
|
|
assert not inp.bip32_paths
|
|
|
|
start_sign(psbt_bytes, finalize=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "WIF store: 0" in story
|
|
signed = end_sign(accept=True, finalize=True)
|
|
|
|
tx_hex = signed.hex()
|
|
accept = bitcoind.rpc.testmempoolaccept([tx_hex])
|
|
assert accept[0]["allowed"], accept
|
|
txid = bitcoind.rpc.sendrawtransaction(tx_hex)
|
|
assert len(txid) == 64
|
|
|
|
settings_remove("wifs")
|
|
goto_home()
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
def test_spend_paper_wallet_addr_only_p2sh_segwit_signed_psbt_finalizes(
|
|
bitcoind, settings_remove, import_wif_to_store, start_sign, end_sign,
|
|
cap_story, use_regtest, pick_menu_item, goto_home, cap_menu, need_keypress,
|
|
load_export):
|
|
use_regtest()
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
amount = 10 # BTC
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
pk = node.node.private_key
|
|
wif_str = pk.wif(testnet=True)
|
|
|
|
import_wif_to_store([wif_str])
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Descriptors")
|
|
pick_menu_item("P2SH-Segwit")
|
|
need_keypress("1") # SD
|
|
desc = load_export("sd", "Descriptor", is_json=False, sig_check=False)
|
|
paper_addr = bitcoind.rpc.deriveaddresses(desc.strip())[0]
|
|
|
|
addr_desc = bitcoind.rpc.getdescriptorinfo("addr(%s)" % paper_addr)["descriptor"]
|
|
paper = bitcoind.create_wallet(wallet_name="paper-addr-p2sh-segwit-signed-psbt",
|
|
disable_private_keys=True, blank=True,
|
|
descriptors=True)
|
|
res = paper.importdescriptors([{
|
|
"desc": addr_desc, "timestamp": "now", "watchonly": True,
|
|
}])
|
|
assert len(res) == 1 and res[0]["success"], res
|
|
|
|
bitcoind.supply_wallet.sendtoaddress(paper_addr, amount)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
assert paper.listunspent()
|
|
|
|
dest = bitcoind.supply_wallet.getnewaddress()
|
|
pubkey_hex = node.node.public_key.sec().hex()
|
|
resp = paper.walletcreatefundedpsbt(
|
|
[], [{dest: amount}], 0,
|
|
{"fee_rate": 3, "subtractFeeFromOutputs": [0],
|
|
"solving_data": {"pubkeys": [pubkey_hex]}})
|
|
psbt_bytes = base64.b64decode(resp["psbt"])
|
|
|
|
po = BasicPSBT().parse(psbt_bytes)
|
|
for inp in po.inputs:
|
|
assert not inp.bip32_paths
|
|
|
|
start_sign(psbt_bytes, finalize=False)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "WIF store: 0" in story
|
|
signed_psbt = end_sign(accept=True, finalize=False)
|
|
|
|
finalize_res = bitcoind.rpc.finalizepsbt(base64.b64encode(signed_psbt).decode(), True)
|
|
assert finalize_res["complete"], finalize_res
|
|
|
|
accept = bitcoind.rpc.testmempoolaccept([finalize_res["hex"]])
|
|
assert accept[0]["allowed"], accept
|
|
|
|
settings_remove("wifs")
|
|
goto_home()
|
|
|
|
|
|
def test_spend_paper_wallet_addr_only_wif_input_details(
|
|
fake_txn, settings_set, settings_remove, start_sign, cap_story,
|
|
pick_menu_item, need_keypress, press_cancel):
|
|
settings_remove("wifs")
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
n = node.subkey_for_path("0/0")
|
|
pubkey_hex = n.node.private_key.K.sec().hex()
|
|
settings_set("wifs", [(pubkey_hex, bytes(n.node.private_key).hex())])
|
|
|
|
def hack(psbt):
|
|
psbt.inputs[0].bip32_paths = None
|
|
psbt.inputs[0].redeem_script = None
|
|
|
|
psbt = fake_txn(1, 1, segwit_in=True, wrapped=True, master_xpub=node.hwif(),
|
|
psbt_hacker=hack)
|
|
|
|
po = BasicPSBT().parse(psbt)
|
|
for inp in po.inputs:
|
|
assert not inp.bip32_paths
|
|
assert inp.redeem_script is None
|
|
|
|
start_sign(psbt, finalize=False)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "OK TO SEND?"
|
|
assert "WIF store: 0" in story
|
|
assert "Press (2) to explore transaction" in story
|
|
|
|
need_keypress("2")
|
|
pick_menu_item("Inputs")
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert title == "Input 0"
|
|
assert "WIF Store" in story
|
|
assert pubkey_hex in story
|
|
|
|
for _ in range(3):
|
|
press_cancel()
|
|
|
|
settings_remove("wifs")
|
|
|
|
|
|
@pytest.mark.bitcoind
|
|
@pytest.mark.parametrize('mode', ["Classic P2PKH", "Segwit P2WPKH", "P2SH-Segwit"])
|
|
def test_spend_paper_wallet_via_electrum(mode, bitcoind, electrum, settings_remove,
|
|
import_wif_to_store, start_sign, end_sign,
|
|
cap_story, use_regtest, pick_menu_item,
|
|
goto_home, cap_menu, press_cancel,
|
|
need_keypress, cap_screen_qr, is_q1):
|
|
use_regtest()
|
|
goto_home()
|
|
settings_remove("wifs")
|
|
amount = 5 # BTC
|
|
|
|
node = BIP32Node.from_master_secret(os.urandom(32))
|
|
pk = node.node.private_key
|
|
wif_str = pk.wif(testnet=True)
|
|
|
|
import_wif_to_store([wif_str])
|
|
menu = cap_menu()
|
|
assert len(menu) == 2
|
|
|
|
pick_menu_item(menu[1])
|
|
pick_menu_item("Addresses")
|
|
pick_menu_item(mode)
|
|
time.sleep(.1)
|
|
need_keypress(KEY_QR if is_q1 else "4")
|
|
time.sleep(.1)
|
|
paper_addr = cap_screen_qr().decode()
|
|
if mode == "Segwit P2WPKH":
|
|
paper_addr = paper_addr.lower()
|
|
|
|
goto_home()
|
|
|
|
# Electrum imported-address watch-only wallet.
|
|
wallet_path = electrum.imported_addr_wallet(
|
|
paper_addr, name="paper-%s" % mode.replace(' ', '-'))
|
|
|
|
# Fund the address via bitcoind, confirm.
|
|
txid = bitcoind.supply_wallet.sendtoaddress(paper_addr, amount)
|
|
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
|
|
# `getrawtransaction` won't see this once it's mined (no -txindex), but
|
|
# the supply wallet still has the tx in its own history.
|
|
funding_hex = bitcoind.supply_wallet.gettransaction(txid)["hex"]
|
|
|
|
# Tell Electrum about the funding tx so its wallet sees the UTXO without
|
|
# needing an Electrum server backend.
|
|
electrum.addtransaction(wallet_path, funding_hex)
|
|
|
|
# Build the unsigned PSBT in Electrum.
|
|
dest = bitcoind.supply_wallet.getnewaddress()
|
|
spend_amt = round(amount - 0.001, 8)
|
|
psbt_b64 = electrum.payto_unsigned_psbt(wallet_path, dest, spend_amt)
|
|
psbt_bytes = base64.b64decode(psbt_b64)
|
|
|
|
# Sanity: confirm Electrum did NOT include any bip32 derivations.
|
|
# If this changes upstream, the test below would no longer be exercising
|
|
# the scriptPubKey auto-detect path.
|
|
po = BasicPSBT().parse(psbt_bytes)
|
|
for i, inp in enumerate(po.inputs):
|
|
assert not inp.bip32_paths
|
|
|
|
# Sign on Coldcard — must use scriptPubKey hash auto-detect.
|
|
start_sign(psbt_bytes, finalize=True)
|
|
time.sleep(.1)
|
|
title, story = cap_story()
|
|
assert "WIF store: 0" in story
|
|
signed = end_sign(accept=True, finalize=True)
|
|
|
|
tx_hex = signed.hex()
|
|
accept = bitcoind.rpc.testmempoolaccept([tx_hex])
|
|
assert accept[0]["allowed"], accept
|
|
txid = bitcoind.rpc.sendrawtransaction(tx_hex)
|
|
assert len(txid) == 64
|
|
|
|
settings_remove("wifs")
|
|
goto_home()
|
|
|
|
# EOF
|