From 64816cbd80fdddddca28638669db0a4eaa9f3dc9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 26 Nov 2018 22:12:18 -0500 Subject: [PATCH 01/10] Remove unneeded BIP 32 test --- test/data/test_bip32.json | 32 -------------------------------- test/test_bip32.py | 24 ------------------------ 2 files changed, 56 deletions(-) delete mode 100644 test/data/test_bip32.json delete mode 100755 test/test_bip32.py diff --git a/test/data/test_bip32.json b/test/data/test_bip32.json deleted file mode 100644 index 404d6f6cf..000000000 --- a/test/data/test_bip32.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "parent": "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", - "child": "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", - "index": 1 - }, - { - "parent": "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", - "child": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", - "index": 2 - }, - { - "parent": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", - "child": "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", - "index": 1000000000 - }, - { - "parent": "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", - "child": "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", - "index": 0 - }, - { - "parent": "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", - "child": "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", - "index": 1 - }, - { - "parent": "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", - "child": "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", - "index": 2 - } -] diff --git a/test/test_bip32.py b/test/test_bip32.py deleted file mode 100755 index 33e78aafd..000000000 --- a/test/test_bip32.py +++ /dev/null @@ -1,24 +0,0 @@ -#! /usr/bin/env python3 - -# Tests in the JSON file are from BIP 32's test vectors. - -import hwilib.bip32 as bip32 -import hwilib.base58 as base58 -import os -import json -import binascii - -print("Starting BIP 32 Public Key Derivation test") - -# Open the data file -with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/test_bip32.json'), encoding='utf-8') as f: - d = json.load(f) - -for test in d: - parent_pk, parent_cc = base58.decompose_xpub(test['parent']) - (child_pk, child_cc) = bip32.CKDpub(parent_pk, parent_cc, test['index']) - real_child_pk, real_child_cc = base58.decompose_xpub(test['child']) - assert(child_pk == real_child_pk) - assert(child_cc == real_child_cc) - -print("Test Completed, passed") From ccd5f2ae1ff55016e27028713c1c125851175cce Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 16:05:02 -0500 Subject: [PATCH 02/10] Use the DebugLink for the Trezor emulator in order to have tests --- hwilib/devices/trezor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 5a818c4ee..042393125 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -2,6 +2,7 @@ from ..hwwclient import HardwareWalletClient from trezorlib.client import TrezorClient as Trezor +from trezorlib.client import TrezorClientDebugLink from trezorlib.transport import enumerate_devices, get_transport from trezorlib import protobuf, tools from trezorlib import messages as proto @@ -12,6 +13,7 @@ import binascii import json +import logging class TxAPIPSBT(TxApi): @@ -50,7 +52,14 @@ class TrezorClient(HardwareWalletClient): def __init__(self, path, password=''): super(TrezorClient, self).__init__(path, password) - self.client = Trezor(transport=get_transport(path)) + if path.startswith('udp'): + logging.debug('Simulator found, using DebugLink') + transport = get_transport(path) + self.client = TrezorClientDebugLink(transport=transport) + debuglink = transport.find_debug() + self.client.set_debuglink(debuglink) + else: + self.client = Trezor(transport=get_transport(path)) # if it wasn't able to find a client, throw an error if not self.client: From 22724282368e1f08618a929d6a168fb3ad39c5c7 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 16:06:14 -0500 Subject: [PATCH 03/10] Add tests for trezor using the emulator Using the trezor emulator, test the enumerate, getxpub, getmasterxpub, getkeypool, and signtx commands. Also tests fingerprint autodetect and device type only specified. --- .gitignore | 1 + test/data/bip32_vectors.json | 54 +++++++ test/test_trezor.py | 277 +++++++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 test/data/bip32_vectors.json create mode 100755 test/test_trezor.py diff --git a/.gitignore b/.gitignore index 66d55e081..a68b33e95 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ build/ dist/ hwi.egg-info/ +test/emulator.img diff --git a/test/data/bip32_vectors.json b/test/data/bip32_vectors.json new file mode 100644 index 000000000..79c43c937 --- /dev/null +++ b/test/data/bip32_vectors.json @@ -0,0 +1,54 @@ +[ + { + "xprv": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + "master_xpub": "xpub6CDEarkRoiwWPj3n3gYygGwgoGchxYg3g6Zs5L2nB4B6wdojzcWCKKHMu9XuY1GyYygRfrVembjAko1T5xTsxj7ecKXxEPzDxx7nCK8Dxtx", + "vectors" : [ + { + "path" : "m/0h", + "xpub" : "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw" + }, + { + "path" : "m/0h/1", + "xpub" : "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ" + }, + { + "path" : "m/0h/1/2h", + "xpub" : "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5" + }, + { + "path" : "m/0h/1/2h/2", + "xpub" : "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV" + }, + { + "path" : "m/0h/1/2h/2/1000000000", + "xpub" : "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy" + } + ] + }, + { + "xprv": "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + "master_xpub": "xpub6DAiPJAHXi5oZE6cXrSgsWdMGKtHW6wCaWsGuYL1Wx9qMtRgJn2VekPQeZc1WwAoeuoytGozkCQnToL2PMw4deyhWGEu7Xou6gPYc1KqYuj", + "vectors" : [ + { + "path" : "m/0", + "xpub" : "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH" + }, + { + "path" : "m/0/2147483647h", + "xpub" : "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a" + }, + { + "path" : "m/0/2147483647h/1", + "xpub" : "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon" + }, + { + "path" : "m/0/2147483647h/1/2147483646h", + "xpub" : "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL" + }, + { + "path" : "m/0/2147483647h/1/2147483646h/2", + "xpub" : "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt" + } + ] + } +] \ No newline at end of file diff --git a/test/test_trezor.py b/test/test_trezor.py new file mode 100755 index 000000000..a84d98399 --- /dev/null +++ b/test/test_trezor.py @@ -0,0 +1,277 @@ +#! /usr/bin/env python3 + +import argparse +import atexit +import logging +import json +import os +import shutil +import socket +import subprocess +import tempfile +import time + +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from trezorlib.transport import enumerate_devices +from trezorlib.transport.udp import UdpTransport +from trezorlib.client import TrezorClientDebugLink +from trezorlib import coins + +from hwilib.commands import process_commands + +parser = argparse.ArgumentParser(description='Test Trezor implementation') +parser.add_argument('emulator', help='Path to the Trezor emulator') +parser.add_argument('bitcoind', help='Path to bitcoind binary') +args = parser.parse_args() + +dev_args = ['-t', 'trezor', '-d', 'udp:127.0.0.1:21324'] + +# Setup logging +logging.basicConfig(format='Trezor Test: %(message)s', level=logging.INFO) + +# Start the Trezor emulator +logging.info('Starting Trezor emulator') +emulator_proc = subprocess.Popen([args.emulator]) +# Wait for emulator to be up +# From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.connect(('127.0.0.1', 21324)) +sock.settimeout(0) +while True: + try: + sock.sendall(b"PINGPING") + r = sock.recv(8) + if r == b"PONGPONG": + break + except Exception: + time.sleep(0.05) +# Cleanup +def cleanup_emulator(): + emulator_proc.kill() +atexit.register(cleanup_emulator) + +# Setup the emulator +for dev in enumerate_devices(): + # Find the udp transport, that's the emulator + if isinstance(dev, UdpTransport): + wirelink = dev + break +debuglink = wirelink.find_debug() +client = TrezorClientDebugLink(wirelink) +client.set_debuglink(debuglink) +client.set_tx_api(coins.tx_api['Bitcoin']) +client.wipe_device() +client.transport.session_begin() +client.load_device_by_mnemonic(mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=False, label='test') # From Trezor device tests + +# Tests! + +# Test enumerate +logging.info('Testing enumerate') +enum_res = process_commands(['enumerate']) +assert(len(enum_res) == 1) +assert('error' not in enum_res[0]) +assert(enum_res[0]['type'] == 'trezor') +assert(enum_res[0]['path'] == 'udp:127.0.0.1:21324') +assert(enum_res[0]['fingerprint'] == '95d8f670') + +# Test path + type +logging.info('Testing path and type not specified') +gmxp_res = process_commands(['getmasterxpub']) +assert('error' in gmxp_res) +assert(gmxp_res['error'] == 'You must specify a device type or fingerprint for all commands except enumerate') +assert('code' in gmxp_res) +assert(gmxp_res['code'] == -1) + +logging.info('Testing path and type specified') +gmxp_res = process_commands(['-t', enum_res[0]['type'], '-d', enum_res[0]['path'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH') + +# Test fingerprint autodetect +logging.info('Testing fingerprint autodetect') +gmxp_res = process_commands(['-f', enum_res[0]['fingerprint'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH') + +# Test device type autodetect +logging.info('Testing only device type specified') +gmxp_res = process_commands(['-t', enum_res[0]['type'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH') + +# Test getxpub +# BIP 32 test vectors +logging.info('Testing getxpub and getmasterxpub') +with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/bip32_vectors.json'), encoding='utf-8') as f: + vectors = json.load(f) +for vec in vectors: + # Setup with xprv + client.wipe_device() + client.load_device_by_xprv(xprv=vec['xprv'], pin='', passphrase_protection=False, label='test', language='english') + + # Test getmasterxpub + gmxp_res = process_commands(dev_args + ['getmasterxpub']) + assert(gmxp_res['xpub'] == vec['master_xpub']) + + # Test the path derivs + for path_vec in vec['vectors']: + gxp_res = process_commands(dev_args + ['getxpub', path_vec['path']]) + assert(gxp_res['xpub'] == path_vec['xpub']) + +# signtx and getkeypool need bitcoind, so start that +logging.info('Setting up bitcoind') +datadir = tempfile.mkdtemp() +bitcoind_proc = subprocess.Popen([args.bitcoind, '-regtest', '-datadir=' + datadir, '-noprinttoconsole']) +def cleanup_bitcoind(): + bitcoind_proc.kill() + shutil.rmtree(datadir) +atexit.register(cleanup_bitcoind) +# Wait for cookie file to be created +while not os.path.exists(datadir + '/regtest/.cookie'): + time.sleep(0.5) +# Read .cookie file to get user and pass +with open(datadir + '/regtest/.cookie') as f: + userpass = f.readline().lstrip().rstrip() +rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(userpass)) + +# Wait for bitcoind to be ready +ready = False +while not ready: + try: + rpc.getblockchaininfo() + ready = True + except JSONRPCException as e: + time.sleep(0.5) + pass + +# Setup bitcoind with no privkey wallet and some blocks +rpc.generatetoaddress(101, rpc.getnewaddress()) +rpc.createwallet('trezor_test', True) +wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/trezor_test'.format(userpass)) +wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(userpass)) + +# Since this is regtest, we need to use regtest in our args +dev_args.append('--testnet') + +# Test getkeypool +logging.info('Testing getkeypool: Importable to privkey enabled wallet') +non_keypool_desc = process_commands(dev_args + ['getkeypool', '0', '20']) +import_result = wpk_rpc.importmulti(non_keypool_desc) +assert(import_result[0]['success']) + +logging.info('Testing getkeypool: Not importable to privkey enabled wallet') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '0', '20']) +import_result = wpk_rpc.importmulti(keypool_desc) +assert(import_result[0]['success'] == False) + +logging.info('Testing getkeypool: Imports to non privkey enabled wallet keypool') +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/44'/1'/0'/0/{}".format(i)) + +logging.info('Testing getkeypool: Imports to non privkey enabled wallet internal keypool') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/44'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --sh_wpkh uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/0'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --wpkh uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/0'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --account uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/3'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/3'/1/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/3'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--internal', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/3'/1/{}".format(i)) + +logging.info('Testing getkeypool: --path uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', 'm/0h/0h/4h/*', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/0'/0'/4'/{}".format(i)) + +logging.info('Testing getkeypool: check --path parse failures') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', '/0h/0h/4h/*', '0', '20']) +assert(keypool_desc['error'] == 'Path must start with m/') +assert(keypool_desc['code'] == -7) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', 'm/0h/0h/4h/', '0', '20']) +assert(keypool_desc['error'] == 'Path must end with /*') +assert(keypool_desc['code'] == -7) + +# Test signtx +logging.info('Testing signtx') +# Import some keys to the watch only wallet and send coins to them +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '30', '40']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '30', '40']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +sh_wpkh_addr = wrpc.getnewaddress('', 'p2sh-segwit') +wpkh_addr = wrpc.getnewaddress('', 'bech32') +pkh_addr = wrpc.getnewaddress('', 'legacy') +wrpc.importaddress(wpkh_addr) +wrpc.importaddress(pkh_addr) +wpk_rpc.sendtoaddress(sh_wpkh_addr, 10) +wpk_rpc.sendtoaddress(wpkh_addr, 10) +wpk_rpc.sendtoaddress(pkh_addr, 10) +wpk_rpc.generatetoaddress(6, wpk_rpc.getnewaddress()) + +# Create a psbt spending the above +psbt = wrpc.walletcreatefundedpsbt([], [{wpk_rpc.getnewaddress():10}], 0, {'includeWatching': True, 'subtractFeeFromOutputs': [0]}, True) +sign_res = process_commands(dev_args + ['signtx', psbt['psbt']]) +finalize_res = wrpc.finalizepsbt(sign_res['psbt']) +assert(finalize_res['complete']) +wrpc.sendrawtransaction(finalize_res['hex']) + +# Done +logging.info('PASS') From d01cbae2b044c31c0997a11438171b2f723a9d4a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 17:05:13 -0500 Subject: [PATCH 04/10] Add tests for Coldcard using Coldcard simulator Using the coldcard simulator, test the enumerate, getxpub, getmasterxpub, and getkeypool commands. Signtx is tested if the simulator is in non-headless mode as it requires user input. Also tests fingerprint autodetect and device type only specified. --- test/data/coldcard_xpubs.json | 47 +++++++ test/test_coldcard.py | 252 ++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 test/data/coldcard_xpubs.json create mode 100755 test/test_coldcard.py diff --git a/test/data/coldcard_xpubs.json b/test/data/coldcard_xpubs.json new file mode 100644 index 000000000..164807d16 --- /dev/null +++ b/test/data/coldcard_xpubs.json @@ -0,0 +1,47 @@ +[ + { + "master_xpub": "tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd", + "vectors" : [ + { + "path" : "m/0h", + "xpub" : "tpubD8NXmKsmWp3Y4WD8VqhKZ61UrvLfghEJjjHsJT8NYBPaHMVDVmCWGmttsn4HE6mEMSdCwcaGtB2iftGFZRcfeF2NxbpzSt2jpkYjdRQuX4W" + }, + { + "path" : "m/0h/1", + "xpub" : "tpubDBKU51v7tZU5KvJ34JHeQ5Rr2uRg8mJLr3JSRR68HPZXEtwubGyqXBDDUqJmUAHzb9h23zyAJVSbzrV6P5qCKfqu8gQfX1EpH16SBeshMXp" + }, + { + "path" : "m/0h/1/2h", + "xpub" : "tpubDD4Y2tN94L2KZiQ1tdpC5oUUATuWAwmRTuPchMY3HCkz2fexHBZDriebcpBYBcicEy1WTYAiALgemNXKd6jcuj9Vj4kUxhG3Uw9m8YgbcTn" + }, + { + "path" : "m/0h/1/2h/2", + "xpub" : "tpubDEt7ap9HgGHPeP5DJqTvuptUw6RoJXfX5bstN8CdSvFgwFa69Qh2mB3hwJ6PAGfvpckRwrazGCJbmeUYtfr5ysREdfqyQSU4UFqhktMCmDr" + }, + { + "path" : "m/0h/1/2h/2/1000000000", + "xpub" : "tpubDGV2stSVNpSZ7BPHmYYTqt4DXPaqKdqGMTX8ecS5Ae5tiVXd2NrqArPV8EBXzZ55y6u86SQF1i4eQ3JVYJbLdGYz3yTN8UrkJQrbgYjEeHH" + }, + { + "path" : "m/0", + "xpub" : "tpubD8NXmKsdB9WZs2HhkE7jEZLymyb5dyvjJg2d7gGJCadyaoogRhtcpTUWdxmvECgVUVpGrxxHJzYdKw6q79NtLL4mzNeqbk4h2B2sE8aisFE" + }, + { + "path" : "m/0/2147483647h", + "xpub" : "tpubDAogKPq6VDemCnuzg2ChNjDFtfiw2bG4Ja17GTxyAmohbNmz29Z7xRitwVdTrUPBdVF1EEuQBrAvUJ6m6kNFKSB723ws3RBxL8a67BnYNd3" + }, + { + "path" : "m/0/2147483647h/1", + "xpub" : "tpubDDAsHiKquEdhD3dpctEhMgrF3QVFKz2NeCS6kkdTcN6AszAyCppLbQDzTqdqzZV4N1guZyWmxpT59dUPWKuKtijGvETuHSbNQubNvZck3L7" + }, + { + "path" : "m/0/2147483647h/1/2147483646h", + "xpub" : "tpubDFNKqVRhj269YBcR4QHUeYHruqHQYCJkQX4K2oJG2S9EEEkvzP2fsEL5RB7zXBspZJNuxvv1qFUDafQQaN7j7Tv2g5U5gzb2YNshNZDR1Fx" + }, + { + "path" : "m/0/2147483647h/1/2147483646h/2", + "xpub" : "tpubDGRkLZjvcoBbAnQjmCcx5Crc6hcU2BBJbXFyX516GSEw68mj6NTB7W7sXnMNA3iVoxDPYTt2QbR3oP8cjxxgkjaN9mqrAP43VqASDa3W4Qz" + } + ] + } +] \ No newline at end of file diff --git a/test/test_coldcard.py b/test/test_coldcard.py new file mode 100755 index 000000000..bc9b0490d --- /dev/null +++ b/test/test_coldcard.py @@ -0,0 +1,252 @@ +#! /usr/bin/env python3 + +import argparse +import atexit +import logging +import json +import os +import shutil +import socket +import subprocess +import tempfile +import time + +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from hwilib.commands import process_commands +from ckcc.protocol import CCProtocolPacker +from ckcc.client import ColdcardDevice + +parser = argparse.ArgumentParser(description='Test Coldcard implementation') +parser.add_argument('simulator', help='Path to the Coldcard simulator') +parser.add_argument('bitcoind', help='Path to bitcoind binary') +args = parser.parse_args() + +dev_args = ['-t', 'coldcard', '-d', '/tmp/ckcc-simulator.sock'] + +# Setup logging +logging.basicConfig(format='Coldcard Test: %(message)s', level=logging.INFO) + +# Start the Coldcard simulator +logging.info('Starting Coldcard simulator') +simulator_proc = subprocess.Popen(['python3', os.path.basename(args.simulator)], cwd=os.path.dirname(args.simulator)) +# Wait for simulator to be up +while True: + enum_res = process_commands(['enumerate']) + if len(enum_res) > 0 and 'error' not in enum_res[0]: + break + time.sleep(0.5) +# Cleanup +def cleanup_simulator(): + dev = ColdcardDevice(sn='/tmp/ckcc-simulator.sock') + resp = dev.send_recv(CCProtocolPacker.logout()) +atexit.register(cleanup_simulator) + +# Tests! + +# Test enumerate +logging.info('Testing enumerate') +enum_res = process_commands(['enumerate']) +assert(len(enum_res) == 1) +assert('error' not in enum_res[0]) +assert(enum_res[0]['type'] == 'coldcard') +assert(enum_res[0]['path'] == '/tmp/ckcc-simulator.sock') +assert(enum_res[0]['fingerprint'] == '0f056943') + +# Test path + type +logging.info('Testing path and type not specified') +gmxp_res = process_commands(['getmasterxpub']) +assert('error' in gmxp_res) +assert(gmxp_res['error'] == 'You must specify a device type or fingerprint for all commands except enumerate') +assert('code' in gmxp_res) +assert(gmxp_res['code'] == -1) + +logging.info('Testing path and type specified') +gmxp_res = process_commands(['-t', enum_res[0]['type'], '-d', enum_res[0]['path'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd') + +# Test fingerprint autodetect +logging.info('Testing fingerprint autodetect') +gmxp_res = process_commands(['-f', enum_res[0]['fingerprint'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd') + +# Test device type autodetect +logging.info('Testing only device type specified') +gmxp_res = process_commands(['-t', enum_res[0]['type'], 'getmasterxpub']) +assert(gmxp_res['xpub'] == 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd') + +# Test getxpub +# BIP 32 test vectors +logging.info('Testing getxpub and getmasterxpub') +with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/coldcard_xpubs.json'), encoding='utf-8') as f: + vectors = json.load(f) +for vec in vectors: + # Test getmasterxpub + gmxp_res = process_commands(dev_args + ['getmasterxpub']) + assert(gmxp_res['xpub'] == vec['master_xpub']) + + # Test the path derivs + for path_vec in vec['vectors']: + gxp_res = process_commands(dev_args + ['getxpub', path_vec['path']]) + assert(gxp_res['xpub'] == path_vec['xpub']) + +# signtx and getkeypool need bitcoind, so start that +logging.info('Setting up bitcoind') +datadir = tempfile.mkdtemp() +bitcoind_proc = subprocess.Popen([args.bitcoind, '-regtest', '-datadir=' + datadir, '-noprinttoconsole']) +def cleanup_bitcoind(): + bitcoind_proc.kill() + shutil.rmtree(datadir) +atexit.register(cleanup_bitcoind) +# Wait for cookie file to be created +while not os.path.exists(datadir + '/regtest/.cookie'): + time.sleep(0.5) +# Read .cookie file to get user and pass +with open(datadir + '/regtest/.cookie') as f: + userpass = f.readline().lstrip().rstrip() +rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(userpass)) + +# Wait for bitcoind to be ready +ready = False +while not ready: + try: + rpc.getblockchaininfo() + ready = True + except JSONRPCException as e: + time.sleep(0.5) + pass + +# Setup bitcoind with no privkey wallet and some blocks +rpc.generatetoaddress(101, rpc.getnewaddress()) +rpc.createwallet('coldcard_test', True) +wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/coldcard_test'.format(userpass)) +wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(userpass)) + +# Since this is regtest, we need to use regtest in our args +dev_args.append('--testnet') + +# Test getkeypool +logging.info('Testing getkeypool: Importable to privkey enabled wallet') +non_keypool_desc = process_commands(dev_args + ['getkeypool', '0', '20']) +import_result = wpk_rpc.importmulti(non_keypool_desc) +assert(import_result[0]['success']) + +logging.info('Testing getkeypool: Not importable to privkey enabled wallet') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '0', '20']) +import_result = wpk_rpc.importmulti(keypool_desc) +assert(import_result[0]['success'] == False) + +logging.info('Testing getkeypool: Imports to non privkey enabled wallet keypool') +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/44'/1'/0'/0/{}".format(i)) + +logging.info('Testing getkeypool: Imports to non privkey enabled wallet internal keypool') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/44'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --sh_wpkh uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/0'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --wpkh uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/0'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--internal', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/0'/1/{}".format(i)) + +logging.info('Testing getkeypool: --account uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/3'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/49'/1'/3'/1/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/3'/0/{}".format(i)) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--wpkh', '--internal', '--account', '3', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getrawchangeaddress()) + assert(addr_info['hdkeypath'] == "m/84'/1'/3'/1/{}".format(i)) + +logging.info('Testing getkeypool: --path uses correct derivation path') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', 'm/0h/0h/4h/*', '0', '20']) +import_result = wrpc.importmulti(keypool_desc) +assert(import_result[0]['success']) +for i in range(0, 21): + addr_info = wrpc.getaddressinfo(wrpc.getnewaddress()) + assert(addr_info['hdkeypath'] == "m/0'/0'/4'/{}".format(i)) + +logging.info('Testing getkeypool: check --path parse failures') +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', '/0h/0h/4h/*', '0', '20']) +assert(keypool_desc['error'] == 'Path must start with m/') +assert(keypool_desc['code'] == -7) +keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--path', 'm/0h/0h/4h/', '0', '20']) +assert(keypool_desc['error'] == 'Path must end with /*') +assert(keypool_desc['code'] == -7) + +# Test signtx +# HACK: Skip this in headless simulator because it requires user input +if not args.simulator.endswith('headless.py'): + logging.info('Testing signtx') + # Import some keys to the watch only wallet and send coins to them + keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '30', '40']) + import_result = wrpc.importmulti(keypool_desc) + assert(import_result[0]['success']) + keypool_desc = process_commands(dev_args + ['getkeypool', '--keypool', '--sh_wpkh', '--internal', '30', '40']) + import_result = wrpc.importmulti(keypool_desc) + assert(import_result[0]['success']) + sh_wpkh_addr = wrpc.getnewaddress('', 'p2sh-segwit') + wpkh_addr = wrpc.getnewaddress('', 'bech32') + pkh_addr = wrpc.getnewaddress('', 'legacy') + wrpc.importaddress(wpkh_addr) + wrpc.importaddress(pkh_addr) + wpk_rpc.sendtoaddress(sh_wpkh_addr, 10) + wpk_rpc.sendtoaddress(wpkh_addr, 10) + wpk_rpc.sendtoaddress(pkh_addr, 10) + wpk_rpc.generatetoaddress(6, wpk_rpc.getnewaddress()) + + # Create a psbt spending the above + psbt = wrpc.walletcreatefundedpsbt([], [{wpk_rpc.getnewaddress():10}], 0, {'includeWatching': True, 'subtractFeeFromOutputs': [0]}, True) + sign_res = process_commands(dev_args + ['signtx', psbt['psbt']]) + finalize_res = wrpc.finalizepsbt(sign_res['psbt']) + assert(finalize_res['complete']) + wrpc.sendrawtransaction(finalize_res['hex']) + +# Done +logging.info('PASS') From a277ac2afd01dbfd1e66b4252a74138ffce976fa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 18:30:29 -0500 Subject: [PATCH 05/10] Fix psbt serializations to always be consistent Sort items before serializing so that the same psbt will always be serialized the same way. --- hwilib/serializations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index dbf4ab843..a2d3ba6cf 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -503,7 +503,7 @@ def DeserializeHDKeypath(f, key, hd_keypaths): def SerializeHDKeypath(hd_keypaths, type): r = b"" - for pubkey, path in hd_keypaths.items(): + for pubkey, path in sorted(hd_keypaths.items()): r += ser_string(type + pubkey) packed = struct.pack("<" + "I" * len(path), *path) r += ser_string(packed) @@ -638,7 +638,7 @@ def serialize(self): r += ser_string(tx) if len(self.final_script_sig) == 0 and self.final_script_witness.is_null(): - for pubkey, sig in self.partial_sigs.items(): + for pubkey, sig in sorted(self.partial_sigs.items()): r += ser_string(b"\x02" + pubkey) r += ser_string(sig) @@ -664,7 +664,7 @@ def serialize(self): r += ser_string(b"\x08") r += self.final_script_witness.serialize() - for key, value in self.unknown: + for key, value in sorted(self.unknown.items()): r += ser_string(key) r += ser_string(value) @@ -745,7 +745,7 @@ def serialize(self): r += SerializeHDKeypath(self.hd_keypaths, b"\x02") - for key, value in self.unknown: + for key, value in sorted(self.unknown.items()): r += ser_string(key) r += ser_string(value) @@ -762,7 +762,7 @@ def __init__(self, tx = None): self.tx = CTransaction() self.inputs = [] self.outputs = [] - self.unknown = [] + self.unknown = {} def deserialize(self, psbt): hexstring = Base64ToHex(psbt.strip()) @@ -860,7 +860,7 @@ def serialize(self): r += b"\x00" # unknowns - for key, value in self.unknown: + for key, value in sorted(self.unknown.items()): r += ser_string(key) r += ser_string(value) From 993b6e8200134221632647f23459e274afcda9b0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 29 Nov 2018 10:18:02 -0500 Subject: [PATCH 06/10] Update test_bech32.py shebang --- test/test_bech32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_bech32.py b/test/test_bech32.py index 14e43ee45..fbaf4e08c 100755 --- a/test/test_bech32.py +++ b/test/test_bech32.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#! /usr/bin/env python3 # Copyright (c) 2017 Pieter Wuille # From 8aa047104ac80fe5f54b4ceb44eff34231922da1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 18:31:12 -0500 Subject: [PATCH 07/10] Add scripts to run tests and prepare test environments Add a run_tests.py script which runs all of the tests. Add a setup_environment.sh script which will download and build the Trezor emulator, Coldcard simulator, and bitcoind. --- .gitignore | 1 + test/data/coldcard-linux-sock.patch | 30 ++++++++ test/run_tests.py | 25 +++++++ test/setup_environment.sh | 108 ++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 test/data/coldcard-linux-sock.patch create mode 100755 test/run_tests.py create mode 100755 test/setup_environment.sh diff --git a/.gitignore b/.gitignore index a68b33e95..155fe7d65 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ dist/ hwi.egg-info/ test/emulator.img +test/work diff --git a/test/data/coldcard-linux-sock.patch b/test/data/coldcard-linux-sock.patch new file mode 100644 index 000000000..b87c0324c --- /dev/null +++ b/test/data/coldcard-linux-sock.patch @@ -0,0 +1,30 @@ +From d1a3a1cef890ebe4ff72a8f89cd6c56dca89747e Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Tue, 27 Nov 2018 17:32:44 -0500 +Subject: [PATCH] Use linux unix socket address format + +--- + unix/frozen-modules/pyb.py | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/unix/frozen-modules/pyb.py b/unix/frozen-modules/pyb.py +index 39778a2..0108516 100644 +--- a/unix/frozen-modules/pyb.py ++++ b/unix/frozen-modules/pyb.py +@@ -23,10 +23,10 @@ class USB_HID: + import usocket as socket + fn = b'/tmp/ckcc-simulator.sock' + self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) +- addr = bytes([len(fn)+2, socket.AF_UNIX] + list(fn)) ++ # addr = bytes([len(fn)+2, socket.AF_UNIX] + list(fn)) + # If on linux, try uncommenting the following two lines +- #import struct +- #addr = struct.pack('H108s', socket.AF_UNIX, fn) ++ import struct ++ addr = struct.pack('H108s', socket.AF_UNIX, fn) + while 1: + try: + self.pipe.bind(addr) +-- +2.11.0 + diff --git a/test/run_tests.py b/test/run_tests.py new file mode 100755 index 000000000..6abdd97c8 --- /dev/null +++ b/test/run_tests.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python3 + +import argparse +import multiprocessing +import os +import subprocess +import sys + +parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests') +trezor_group = parser.add_mutually_exclusive_group() +trezor_group.add_argument('--no_trezor', help='Do not run Trezor test with emulator', action='store_true') +trezor_group.add_argument('--trezor', help='Path to Trezor emulator.', default='work/trezor-mcu/firmware/trezor.elf') +coldcard_group = parser.add_mutually_exclusive_group() +coldcard_group.add_argument('--no_coldcard', help='Do not run Coldcard test with simulator', action='store_true') +coldcard_group.add_argument('--coldcard', help='Path to Coldcard simulator.', default='work/firmware/unix/headless.py') +parser.add_argument('--bitcoind', help='Path to bitcoind.', default='work/bitcoin/src/bitcoind') +args = parser.parse_args() + +# Run tests +subprocess.check_call(['python3', 'test_bech32.py']) +subprocess.check_call(['python3', 'test_psbt.py']) +if not args.no_trezor: + subprocess.check_call(['python3', 'test_trezor.py', args.trezor, args.bitcoind]) +if not args.no_coldcard: + subprocess.check_call(['python3', 'test_coldcard.py', args.coldcard, args.bitcoind]) diff --git a/test/setup_environment.sh b/test/setup_environment.sh new file mode 100755 index 000000000..6abd74d26 --- /dev/null +++ b/test/setup_environment.sh @@ -0,0 +1,108 @@ +#! /usr/bin/env bash + +# Makes debugging easier +set -x + +# Go into the working directory +mkdir -p work +cd work + +# Clone trezor-mcu if it doesn't exist, or update it if it does +trezor_setup_needed=false +if [ ! -d "trezor-mcu" ]; then + git clone --recursive https://github.com/trezor/trezor-mcu.git + cd trezor-mcu + trezor_setup_needed=true +else + cd trezor-mcu + git fetch + + # Determine if we need to pull. From https://stackoverflow.com/a/3278427 + UPSTREAM=${1:-'@{u}'} + LOCAL=$(git rev-parse @) + REMOTE=$(git rev-parse "$UPSTREAM") + BASE=$(git merge-base @ "$UPSTREAM") + + if [ $LOCAL = $REMOTE ]; then + echo "Up-to-date" + elif [ $LOCAL = $BASE ]; then + git pull + trezor_setup_needed=true + fi +fi + +# Build emulator. This is pretty fast, so rebuilding every time is ok +# But there should be some caching that makes this faster +export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 +if [ "$trezor_setup_needed" == true ] ; then + script/setup + pipenv install +fi +pipenv run script/cibuild +cd .. + +# Clone coldcard firmware if it doesn't exist, or update it if it does +coldcard_setup_needed=false +if [ ! -d "firmware" ]; then + git clone --recursive https://github.com/Coldcard/firmware.git + cd firmware + coldcard_setup_needed=true +else + cd firmware + git reset --hard HEAD^ # Undo git-am for checking and updating + git fetch + + # Determine if we need to pull. From https://stackoverflow.com/a/3278427 + UPSTREAM=${1:-'@{u}'} + LOCAL=$(git rev-parse @) + REMOTE=$(git rev-parse "$UPSTREAM") + BASE=$(git merge-base @ "$UPSTREAM") + + if [ $LOCAL = $REMOTE ]; then + echo "Up-to-date" + elif [ $LOCAL = $BASE ]; then + git pull + coldcard_setup_needed=true + fi +fi +# Apply patch to make simulator work in linux environments +git am ../../data/coldcard-linux-sock.patch + +# Build the simulator. This is cached, but it is also fast +cd unix +if [ "$coldcard_setup_needed" == true ] ; then + make setup +fi +make -j$(nproc) +cd ../.. + +# Clone bitcoind if it doesn't exist, or update it if it does +bitcoind_setup_needed=false +if [ ! -d "bitcoin" ]; then + git clone https://github.com/achow101/bitcoin.git -b hww + cd bitcoin + bitcoind_setup_needed=true +else + cd bitcoin + git fetch + + # Determine if we need to pull. From https://stackoverflow.com/a/3278427 + UPSTREAM=${1:-'@{u}'} + LOCAL=$(git rev-parse @) + REMOTE=$(git rev-parse "$UPSTREAM") + BASE=$(git merge-base @ "$UPSTREAM") + + if [ $LOCAL = $REMOTE ]; then + echo "Up-to-date" + elif [ $LOCAL = $BASE ]; then + git pull + bitcoind_setup_needed=true + fi +fi + +# Build bitcoind. This is super slow, but it is cached so it runs fairly quickly. +if [ "$bitcoind_setup_needed" == true ] ; then + ./autogen.sh + ./configure +fi +make -j$(nproc) src/bitcoind From ddcf842e5953fccd3aab254b5e78bca208468785 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 29 Nov 2018 10:41:19 -0500 Subject: [PATCH 08/10] extra for tests dependencies Adds an extra_requires for the tests which has the dependencies needed for running the tests (besides emulator/simulator deps). --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index be9801fda..71498087c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/achow101/hwi", - packages=setuptools.find_packages(exclude=['dopcs', 'test']), + packages=setuptools.find_packages(exclude=['docs', 'test']), install_requires=[ 'hidapi', # HID API needed in general 'trezor[hidapi]', # Trezor One @@ -27,4 +27,7 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], + extras_require={ + 'tests': ['python-bitcoinrpc'] + } ) From ced308796c329cf6f5d2255445f3b3fa70ef64ac Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 18:56:54 -0500 Subject: [PATCH 09/10] Document tests and how to run them --- test/README.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/README.md diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..6c04c170b --- /dev/null +++ b/test/README.md @@ -0,0 +1,102 @@ +# HWI Tests + +## Running the tests + +This folder contains test cases for HWI. To run these tests, `hwilib` will need to be installed to your python system. You can install it by doing `pip install -e .[tests]` in the root directory. + +- `test_bech32.py` tests the bech32 serialization. +This is taken directly from the [python reference implementation](https://github.com/sipa/bech32/blob/master/ref/python/tests.py). +- `test_psbt.py` tests the psbt serialization. +It implements all of the [BIP 174 serialization test vectors](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Test_Vectors). +- `test_trezor.py` tests the command line interface and the Trezor implementation. +It uses the [Trezor One firmware emulator](https://github.com/trezor/trezor-mcu/#building-for-development). +It also tests usage with `bitcoind`, so the [patched Bitcoin Core](../docs/bitcoin-core-usage.md#bitcoin-core) is required. +- `test_coldcard.py` tests the command line interface and Coldcard implementation. +It uses the [Coldcard simulator](https://github.com/Coldcard/firmware/tree/master/unix#coldcard-desktop-simulator). +It also tests usage with `bitcoind`, so the [patched Bitcoin Core](../docs/bitcoin-core-usage.md#bitcoin-core) is required. + +`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, and the patched `bitcoind`. +if run in the `test/` directory, these will be built in `work/test/trezor-mcu`, `work/test/firmware`, and `work/test/bitcoin` respectively. + +`run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, and bitcoind. +Otherwise the paths to those will need to be specified on the command line. +test_trezor.py` and `test_coldcard.py` can be disabled. + +If you are building the Trezor emulator, the Coldcard simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`. + +``` +$ cd test +$ mkdir -p work +$ cd work +``` + +## Trezor emulator + +### Dependencies + +In order to build the Trezor emulator, the following packages will need to be installed: + +``` +build-essential curl git python3 python3-pip libsdl2-dev libsdl2-image-dev gcc-arm-none-eabi libnewlib-arm-none-eabi gcc-multilib +``` + +The python packages can be installed with + +``` +pip install pipenv +``` + +### Building + +Clone the repository: + +``` +$ git clone https://github.com/trezor/trezor-mcu/ +``` + +Build the emulator in headless mode: + +``` +$ cd trezor-mcu +$ export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 +$ script/setup +$ pipenv install +$ pipenv run script/cibuild +``` + +## Coldcard simulator + +### Dependencies + +In order to build the Coldcard simulator, the following packages will need to be installed: + +``` +build-essential git python3 python3-pip libudev-dev gcc-arm-none-eabi +``` + +After cloninig the Coldcard repo into this testing folder, the python packages can be installed with: + +``` +pip install -r ckcc_firmware/requirements.txt +pip install -r ckcc_firmware/unix/requirements.txt +``` + +### Building + +Clone the repository: + +``` +$ git clone https://github.com/coldcard/firmware +``` + +Build the emulator in headless mode: + +``` +$ cd firmware/unix +$ make setup +$ make +``` + +## Bitcoin Core + +In order to build `bitcoind`, see [Bitcoin Core's build documentation](https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md#linux-distribution-specific-instructions) to get all of the dependencies installed and for instructions on how to build. From fd19704686f4f3898a661d77c35a0f8f9e712f40 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 27 Nov 2018 19:30:04 -0500 Subject: [PATCH 10/10] Travis config --- .travis.yml | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 55 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..18bfd5c68 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +language: python +os: linux +dist: xenial +python: + - '3.5' +cache: + pip: true + ccache: true + directories: + - test/work +addons: + apt: + sources: + - sourceline: 'ppa:bitcoin/bitcoin' + packages: + - libdb4.8-dev + - libdb4.8++-dev + - build-essential + - curl + - git + - libsdl2-dev + - libsdl2-image-dev + - gcc-arm-none-eabi + - libnewlib-arm-none-eabi + - libudev-dev + - libtool + - autotools-dev + - automake + - pkg-config + - bsdmainutils + - libssl-dev + - libevent-dev + - libboost-system-dev + - libboost-filesystem-dev + - libboost-chrono-dev + - libboost-test-dev + - libboost-thread-dev + - libusb-1.0-0-dev + - protobuf-compiler + - cython3 + - ccache +install: + - pip install pipenv pysdl2 python-bitcoinrpc + # From trezor-mcu to get the correct protobuf version + - curl -LO "https://github.com/google/protobuf/releases/download/v3.4.0/protoc-3.4.0-linux-x86_64.zip" + - unzip "protoc-3.4.0-linux-x86_64.zip" -d protoc + - export PATH="$(pwd)/protoc/bin:$PATH" + # Build emulators/simulators and bitcoind + - cd test; ./setup_environment.sh; cd .. + - pip uninstall -y trezor # Hack to get rid of master branch version of trezor that is installed for trezor-mcu build + - python setup.py install +script: + - cd test; ./run_tests.py diff --git a/README.md b/README.md index a951b3fb1..60397674f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Bitcoin Hardware Wallet Interaction scripts +[![Build Status](https://travis-ci.org/achow101/HWI.svg?branch=master)](https://travis-ci.org/achow101/HWI) + This project contains several scripts for interacting with Bitcoin hardware wallets. ## Prerequisites