firmware/testing/test_ownership.py

575 lines
19 KiB
Python

# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Address ownership tests.
#
import pytest, time, io, csv
from txn import fake_address
from base58 import encode_base58_checksum
from bech32 import encode as bech32_encode
from helpers import hash160, addr_from_display_format
from bip32 import BIP32Node
from constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from constants import simulator_fixed_xprv, simulator_fixed_tprv, addr_fmt_names
from charcodes import KEY_QR
@pytest.fixture
def wipe_cache(sim_exec):
def doit():
cmd = f'from ownership import OWNERSHIP; OWNERSHIP.wipe_all();'
sim_exec(cmd)
return doit
'''
>>> [AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH]
[14, 8, 26, 1, 7, 19]
'''
@pytest.mark.parametrize('addr_fmt', [
AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
])
@pytest.mark.parametrize('testnet', [ False, True] )
def test_negative(addr_fmt, testnet, sim_exec):
# unit test, no UX
addr = fake_address(addr_fmt, testnet)
cmd = f'from ownership import OWNERSHIP; w,path=OWNERSHIP.search({addr!r}); '\
'RV.write(repr([w.name, path]))'
lst = sim_exec(cmd)
assert 'Explained' in lst
@pytest.mark.parametrize('addr_fmt, testnet', [
(AF_CLASSIC, True),
(AF_CLASSIC, False),
(AF_P2WPKH, True),
(AF_P2WPKH, False),
(AF_P2WPKH_P2SH, True),
(AF_P2WPKH_P2SH, False),
# multisig - testnet only
(AF_P2WSH, True),
(AF_P2SH, True),
(AF_P2WSH_P2SH,True),
])
@pytest.mark.parametrize('offset', [ 3, 760, 763] )
@pytest.mark.parametrize('subaccount', [ 0, 34] )
@pytest.mark.parametrize('change_idx', [ 0, 1] )
@pytest.mark.parametrize('from_empty', [ True, False] )
def test_positive(addr_fmt, offset, subaccount, testnet, from_empty, change_idx,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
enter_number, press_cancel, settings_set, import_ms_wallet, clear_ms
):
# API/Unit test, limited UX
if not testnet and addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
# multisig jigs assume testnet
raise pytest.skip('testnet only')
wipe_cache()
settings_set('accts', [])
use_testnet(testnet)
coin_type = 1 if testnet else 0
if addr_fmt in { AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH }:
from test_multisig import make_ms_address, HARD
M, N = 1, 3
expect_name = f'search-test-{addr_fmt}'
clear_ms()
keys = import_ms_wallet(M, N, name=expect_name, accept=1, addr_fmt=addr_fmt_names[addr_fmt])
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
addr, scriptPubKey, script, details = make_ms_address(M, keys,
is_change=change_idx, idx=offset, addr_fmt=addr_fmt, testnet=int(testnet),
path_mapper=lambda cosigner: [HARD(45), change_idx, offset])
path = f'.../{change_idx}/{offset}'
else:
if addr_fmt == AF_CLASSIC:
menu_item = expect_name = 'Classic P2PKH'
path = "m/44h/{ct}h/{acc}h"
elif addr_fmt == AF_P2WPKH_P2SH:
menu_item = expect_name = 'P2SH-Segwit'
path = "m/49h/{ct}h/{acc}h"
clear_ms()
elif addr_fmt == AF_P2WPKH:
menu_item = expect_name = 'Segwit P2WPKH'
path = "m/84h/{ct}h/{acc}h"
else:
raise ValueError(addr_fmt)
path_prefix = path.format(ct=coin_type, acc=subaccount)
path = path_prefix + f'/{change_idx}/{offset}'
print(f'path = {path}')
# see addr_vs_path
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
sk = mk.subkey_for_path(path[2:])
if addr_fmt == AF_CLASSIC:
addr = sk.address(netcode="XTN" if testnet else "BTC")
elif addr_fmt == AF_P2WPKH_P2SH:
pkh = sk.hash160()
digest = hash160(b'\x00\x14' + pkh)
addr = encode_base58_checksum(bytes([196 if testnet else 5]) + digest)
else:
pkh = sk.hash160()
addr = bech32_encode('tb' if testnet else 'bc', 0, pkh)
if subaccount:
# need to hint we're doing a non-zero acccount number
goto_home()
settings_set('axskip', True)
pick_menu_item('Address Explorer')
pick_menu_item('Account Number')
enter_number(subaccount)
pick_menu_item(menu_item)
press_cancel()
cmd = (f'from ownership import OWNERSHIP;'
f'c,w,path=OWNERSHIP.search({addr!r});'
f'RV.write(repr([c, w.name, path]))')
if not from_empty:
# we expect here to find address from cache
# so we first need to generate proper cache
lst = sim_exec(cmd, timeout=None)
assert 'Traceback' not in lst, lst
lst = eval(lst)
assert len(lst) == 3
assert lst[0] is False # not from cache, needed to build it
lst = sim_exec(cmd, timeout=None)
assert 'Traceback' not in lst, lst
lst = eval(lst)
assert len(lst) == 3
from_cache, got_name, got_path = lst
assert from_cache == (not from_empty)
assert expect_name in got_name
if subaccount and '...' not in path:
# not expected for multisig, since we have proper wallet name
assert f'Account#{subaccount}' in got_name
assert got_path == (change_idx, offset)
@pytest.mark.parametrize('valid', [ True, False] )
@pytest.mark.parametrize('testnet', [ True, False] )
@pytest.mark.parametrize('method', [ 'qr', 'nfc'] )
@pytest.mark.parametrize('multisig', [ True, False] )
def test_ux(valid, testnet, method,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
press_cancel, press_select, settings_set, is_q1, nfc_write, need_keypress,
cap_screen, cap_story, load_shared_mod, scan_a_qr, skip_if_useless_way,
sign_msg_from_address, multisig, import_ms_wallet, clear_ms, verify_qr_address,
src_root_dir, sim_root_dir
):
skip_if_useless_way(method)
addr_fmt = AF_CLASSIC
if valid:
if multisig:
from test_multisig import make_ms_address, HARD
M, N = 2, 3
expect_name = f'own_ux_test'
clear_ms()
keys = import_ms_wallet(M, N, AF_P2WSH, name=expect_name, accept=1)
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
addr, scriptPubKey, script, details = make_ms_address(
M, keys, is_change=0, idx=50, addr_fmt=AF_P2WSH,
testnet=int(testnet), path_mapper=lambda cosigner: [HARD(45), 0, 50]
)
addr_fmt = AF_P2WSH
else:
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv if testnet else simulator_fixed_xprv)
path = "m/44h/{ct}h/{acc}h/0/3".format(acc=0, ct=(1 if testnet else 0))
sk = mk.subkey_for_path(path)
addr = sk.address(netcode="XTN" if testnet else "BTC")
else:
addr = fake_address(addr_fmt, testnet)
if method == 'qr':
goto_home()
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')
elif method == 'nfc':
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()
# run simulator w/ --set nfc=1 --eff
goto_home()
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)
#press_select()
else:
raise ValueError(method)
time.sleep(1)
title, story = cap_story()
assert addr == addr_from_display_format(story.split("\n\n")[0])
if title == 'Unknown Address' and not testnet:
assert 'That address is not valid on Bitcoin Testnet' in story
elif valid:
assert title == ('Verified Address' if is_q1 else "Verified!")
assert 'Found in wallet' in story
assert 'Derivation path' in story
if is_q1:
# check it can display as QR from here
need_keypress(KEY_QR)
verify_qr_address(addr_fmt, addr)
press_cancel()
if multisig:
assert expect_name in story
assert "Press (0) to sign message with this key" not in story
else:
assert 'P2PKH' in story
assert "Press (0) to sign message with this key" in story
need_keypress('0')
msg = "coinkite CC the most solid HWW"
sign_msg_from_address(msg, addr, path, addr_fmt, method, testnet)
else:
assert title == 'Unknown Address'
assert 'Searched 1528' in story # max
assert "1 wallet(s)" in story
assert 'without finding a match' in story
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "ms0"])
def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explorer,
pick_menu_item, need_keypress, sim_exec, clear_ms,
import_ms_wallet, press_select, goto_home, nfc_write,
load_shared_mod, load_export_and_verify_signature,
cap_story, is_q1, src_root_dir, sim_root_dir, settings_remove):
goto_home()
wipe_cache()
settings_set('accts', [])
settings_set('msas', 1)
if af == "ms0":
clear_ms()
import_ms_wallet(2,3, name=af)
press_select() # accept ms import
goto_address_explorer()
pick_menu_item(af)
need_keypress("1") # save to SD
cmd = f'import os; RV.write(repr([i for i in os.listdir() if ".own" in i]))'
lst = sim_exec(cmd)
assert 'Traceback' not in lst, lst
lst = eval(lst)
assert lst
title, body = cap_story()
contents, sig_addr, _ = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
if af == "ms0":
for i in ['Index', 'Payment Address', 'Redeem Script']:
assert i in hdr
else:
assert hdr == ['Index', 'Payment Address', 'Derivation']
addr = None
for n, rv in enumerate(cc, start=0):
assert int(rv[0]) == n
if int(rv[0]) == 200:
addr = rv[1]
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()
# run simulator w/ --set nfc=1 --eff
goto_home()
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 title == ('Verified Address' if is_q1 else "Verified!")
assert 'Found in wallet' in story
assert 'Derivation path' in story
if af == "Segwit P2WPKH":
assert " P2WPKH " in story
else:
assert af in story
settings_remove("msas")
def test_ae_saver(wipe_cache, settings_set, goto_address_explorer, cap_story,
pick_menu_item, need_keypress, sim_exec, clear_ms, is_q1,
import_ms_wallet, press_select, goto_home, nfc_write,
load_shared_mod, load_export_and_verify_signature,
set_addr_exp_start_idx, use_testnet):
cmd = lambda a: (
f'from ownership import OWNERSHIP;'
f'c,w,path=OWNERSHIP.search({a!r});'
f'RV.write(repr([c, w.name, path]))')
def cache_check(a, from_cache):
l = sim_exec(cmd(a), timeout=None)
assert 'Traceback' not in l, l
assert eval(l)[0] == from_cache
use_testnet()
goto_home()
wipe_cache()
settings_set('accts', [])
settings_set('aei', True)
goto_address_explorer()
set_addr_exp_start_idx(7) # starting from index 7
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
contents, sig_addr, _ = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
assert hdr == ['Index', 'Payment Address', 'Derivation']
addrs = {}
for idx, addr, deriv in cc:
addrs[int(idx)] = addr
# nothing was created from above as start index was 7
cache_check(addrs[7], False)
# now we have cached addresses up to 27
for i in range(8, 28):
cache_check(addrs[i], True)
# cache file position at 27 (aka count)
goto_address_explorer()
set_addr_exp_start_idx(1) # starting from index 1
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
load_export_and_verify_signature(body, "sd", label="Address summary")
# after above we must have first 250 addresses cached
cache_check(addrs[249], True)
# cache file position at 250 (aka count)
goto_address_explorer()
set_addr_exp_start_idx(250) # starting from index 250
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
contents, sig_addr, _ = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
assert hdr == ['Index', 'Payment Address', 'Derivation']
addrs = {}
for idx, addr, deriv in cc:
addrs[int(idx)] = addr
# after above we must have first 500 addresses cached
cache_check(addrs[300], True)
cache_check(addrs[400], True)
cache_check(addrs[499], True)
# now addresses that we already have, does nothing
goto_address_explorer()
set_addr_exp_start_idx(100) # starting from index 100
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
load_export_and_verify_signature(body, "sd", label="Address summary")
cache_check(addrs[499], True)
# now move count up via ownership
mk = BIP32Node.from_wallet_key(simulator_fixed_tprv)
sk = mk.subkey_for_path("84h/1h/0h/0/580")
addr = bech32_encode('tb', 0, sk.hash160())
cache_check(addr, False)
# now count at 600 (580+20)
# now over the max but with some we already have
goto_address_explorer()
set_addr_exp_start_idx(550) # starting from index 550 (would go up to 800)
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
contents, sig_addr, _ = load_export_and_verify_signature(body, "sd", label="Address summary")
addr_dump = io.StringIO(contents)
cc = csv.reader(addr_dump)
hdr = next(cc)
assert hdr == ['Index', 'Payment Address', 'Derivation']
addrs = {}
for idx, addr, deriv in cc:
addrs[int(idx)] = addr
assert 799 in addrs
cache_check(addrs[763], True) # max
# start idx over max stored addresses
goto_address_explorer()
set_addr_exp_start_idx(764) # starting from index 764
pick_menu_item("Segwit P2WPKH")
need_keypress("1") # save to SD
time.sleep(.1)
title, body = cap_story()
load_export_and_verify_signature(body, "sd", label="Address summary")
# does notthing harmful, nothing added
cache_check(addrs[763], True) # max
l = sim_exec(cmd(addrs[764]), timeout=None)
assert 'Traceback' in l
assert 'Searched 1528' in l # max
assert "1 wallet(s)" in l
assert 'without finding a match' in l
def test_regtest_addr_on_mainnet(goto_home, is_q1, pick_menu_item, scan_a_qr, nfc_write, cap_story,
need_keypress, load_shared_mod, use_mainnet, src_root_dir, sim_root_dir):
# testing bug in chains.possible_address_fmt
# allowed regtest addresses to be allowed on main chain
goto_home()
use_mainnet()
addr = "bcrt1qmff7njttlp6tqtj0nq7svcj2p9takyqm3mfl06"
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()
# run simulator w/ --set nfc=1 --eff
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)
# press_select()
time.sleep(1)
title, story = cap_story()
assert addr == addr_from_display_format(story.split("\n\n")[0])
assert title == 'Unknown Address'
assert "not valid on Bitcoin Mainnet" in story
def test_20_more_build_after_match(sim_exec, import_ms_wallet, clear_ms, wipe_cache, settings_set):
from test_multisig import make_ms_address, HARD
cmd = lambda a: (
f'from ownership import OWNERSHIP;'
f'c,w,path=OWNERSHIP.search({a!r});'
f'RV.write(repr([c, w.name, path]))')
# create multisig wallet
M, N = 2, 3
expect_name = 'test20more'
clear_ms()
keys = import_ms_wallet(M, N, name=expect_name, accept=True, addr_fmt="p2wsh")
make_a = lambda index: make_ms_address(
M, keys,
is_change=False, idx=index, addr_fmt=AF_P2WSH, testnet=True,
path_mapper=lambda cosigner: [HARD(45), 0, index])
def cache_check(index, from_cache):
a = make_a(index)[0]
l = sim_exec(cmd(a), timeout=None)
assert 'Traceback' not in l, l
assert eval(l)[0] == from_cache
# clean slate
wipe_cache()
settings_set('accts', [])
# generate 10th (idx=9) address (external)
# first run, generated first 10 addresses + 20
cache_check(9, False)
# now we can go up to index 29 - all must come from cache
for i in range(10, 30):
cache_check(i, True)
# idx 30 - not in cache
# but will cache next 20 addrs
cache_check(30, False)
# now we can go up to index 51 - all must come from cache
for i in range(31, 51):
cache_check(i, True)
# idx 51 - not in cache
# but will cache next 20 addrs
cache_check(51, False)
cache_check(760, False)
cache_check(761, True)
cache_check(762, True)
cache_check(763, True)
# after max - not gonna find
addr = make_a(764)[0]
l = sim_exec(cmd(addr), timeout=None)
assert 'Traceback' in l
assert 'Searched 1528' in l # max
assert "1 wallet(s)" in l
assert 'without finding a match' in l
# EOF