From d4b1582e5cec46cbe61817f6d72b1c0cb00ddd90 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 7 Oct 2019 14:20:26 -0400 Subject: [PATCH 001/634] Build libusb 1.0.23 for Windows binaries libusb 1.0.23 is needed for Windows 10 to work, but pre-build binaries are not provided for it, so we need to build our own deterministically. --- contrib/build.Dockerfile | 3 ++- contrib/build_wine.sh | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index e15839ada..3303c151c 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -27,7 +27,8 @@ RUN apt-get install -y \ libudev-dev \ faketime \ zip \ - dos2unix + dos2unix \ + g++-mingw-w64-x86-64 RUN curl https://pyenv.run | bash ENV PATH="/root/.pyenv/bin:$PATH" diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 59d6cffe4..b441be0f8 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -9,8 +9,9 @@ PYTHON_FOLDER="python3" PYHOME="c:/$PYTHON_FOLDER" PYTHON="wine $PYHOME/python.exe -OO -B" -LIBUSB_URL=https://github.com/libusb/libusb/releases/download/v1.0.22/libusb-1.0.22.7z -LIBUSB_HASH="671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b" +LIBUSB_VERSION=1.0.23 +LIBUSB_URL=https://github.com/libusb/libusb/releases/download/v1.0.23/libusb-1.0.23.tar.bz2 +LIBUSB_HASH="db11c06e958a82dac52cf3c65cb4dd2c3f339c8a988665110e0d24d19312ad8d" WINDOWS_SDK_URL=http://go.microsoft.com/fwlink/p/?LinkID=2033686 WINDOWS_SDK_HASH="016981259708e1afcab666c7c1ff44d1c4d63b5e778af8bc41b4f6db3d27961a" @@ -33,11 +34,15 @@ for msifile in core dev exe lib pip tools; do rm $msifile.msi* done -# Get libusb -wget -N -c -O libusb.7z "$LIBUSB_URL" -echo "$LIBUSB_HASH libusb.7z" | sha256sum -c -7za x -olibusb libusb.7z -aoa -cp libusb/MS64/dll/libusb-1.0.dll ~/.wine/drive_c/python3/ +# Get and build libusb +wget -N -c -O libusb.tar.bz2 "$LIBUSB_URL" +echo "$LIBUSB_HASH libusb.tar.bz2" | sha256sum -c +tar -xf libusb.tar.bz2 +pushd "libusb-$LIBUSB_VERSION" +./configure --host=x86_64-w64-mingw32 +faketime -f "2019-01-01 00:00:00" make +cp libusb/.libs/libusb-1.0.dll ~/.wine/drive_c/python3/ +popd rm -r libusb* # Get the Windows SDK From ed932e20d867b806a1c9a178b630502a56500313 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 7 Oct 2019 23:57:51 -0400 Subject: [PATCH 002/634] Add get_usb_vendor_id function to transports --- hwilib/devices/trezorlib/transport/__init__.py | 3 +++ hwilib/devices/trezorlib/transport/hid.py | 3 +++ hwilib/devices/trezorlib/transport/webusb.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/hwilib/devices/trezorlib/transport/__init__.py b/hwilib/devices/trezorlib/transport/__init__.py index b2d19feae..6b4627f93 100644 --- a/hwilib/devices/trezorlib/transport/__init__.py +++ b/hwilib/devices/trezorlib/transport/__init__.py @@ -67,6 +67,9 @@ def __str__(self) -> str: def get_path(self) -> str: raise NotImplementedError + def get_usb_vendor_id(self) -> int: + return -1 + def begin_session(self) -> None: raise NotImplementedError diff --git a/hwilib/devices/trezorlib/transport/hid.py b/hwilib/devices/trezorlib/transport/hid.py index 68849ddbb..8ab443818 100644 --- a/hwilib/devices/trezorlib/transport/hid.py +++ b/hwilib/devices/trezorlib/transport/hid.py @@ -125,6 +125,9 @@ def __init__(self, device: HidDevice) -> None: def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, self.device["path"].decode()) + def get_usb_vendor_id(self) -> int: + return self.device["vendor_id"] + @classmethod def enumerate(cls, debug: bool = False) -> Iterable["HidTransport"]: devices = [] diff --git a/hwilib/devices/trezorlib/transport/webusb.py b/hwilib/devices/trezorlib/transport/webusb.py index 61d14e4a2..078e21f5e 100644 --- a/hwilib/devices/trezorlib/transport/webusb.py +++ b/hwilib/devices/trezorlib/transport/webusb.py @@ -105,6 +105,9 @@ def __init__( def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, dev_to_str(self.device)) + def get_usb_vendor_id(self) -> int: + return self.device.getVendorID() + @classmethod def enumerate(cls) -> Iterable["WebUsbTransport"]: if cls.context is None: From da6bde5ae0467eb61f68f286b54b52c9a4f17315 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 7 Oct 2019 23:58:05 -0400 Subject: [PATCH 003/634] Do additional filtering on usb ids for Trezor and Keepkey enumerate When determining whether a device is Trezor or Keepkey, explicitly filter out the other one (i.e. Trezor explicilty filters out Keepkey and Keepkey vice versa) using the usb id. Unknown ones (i.e. emulators because they have no usb id) will go through the original, less reliable, filtering method of checking the vendor information from the Features message. --- hwilib/devices/keepkey.py | 6 +++++- hwilib/devices/trezor.py | 6 +++++- hwilib/devices/trezorlib/transport/__init__.py | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 6de1ccc36..3c0bd900a 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -1,7 +1,7 @@ # KeepKey interaction script from ..errors import DEVICE_NOT_INITIALIZED, DeviceNotReadyError, common_err_msgs, handle_errors -from .trezorlib.transport import enumerate_devices +from .trezorlib.transport import enumerate_devices, KEEPKEY_VENDOR_IDS from .trezor import TrezorClient from ..base58 import get_xpub_fingerprint_hex @@ -15,6 +15,10 @@ def __init__(self, path, password=''): def enumerate(password=''): results = [] for dev in enumerate_devices(): + # enumerate_devices filters to Trezors and Keepkeys. + # Only allow Keepkeys and unknowns. Unknown devices will reach the check for vendor later + if dev.get_usb_vendor_id() not in KEEPKEY_VENDOR_IDS | {-1}: + continue d_data = {} d_data['type'] = 'keepkey' diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index a2a4b983d..14b2147ab 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -5,7 +5,7 @@ from .trezorlib.client import TrezorClient as Trezor from .trezorlib.debuglink import TrezorClientDebugLink from .trezorlib.exceptions import Cancelled -from .trezorlib.transport import enumerate_devices, get_transport +from .trezorlib.transport import enumerate_devices, get_transport, TREZOR_VENDOR_IDS from .trezorlib.ui import echo, PassphraseUI, mnemonic_words, PIN_CURRENT, PIN_NEW, PIN_CONFIRM, PIN_MATRIX_DESCRIPTION, prompt from .trezorlib import tools, btc, device from .trezorlib import messages as proto @@ -424,6 +424,10 @@ def send_pin(self, pin): def enumerate(password=''): results = [] for dev in enumerate_devices(): + # enumerate_devices filters to Trezors and Keepkeys. + # Only allow Trezors and unknowns. Unknown devices will reach the check for vendor later + if dev.get_usb_vendor_id() not in TREZOR_VENDOR_IDS | {-1}: + continue d_data = {} d_data['type'] = 'trezor' diff --git a/hwilib/devices/trezorlib/transport/__init__.py b/hwilib/devices/trezorlib/transport/__init__.py index 6b4627f93..8a7685b38 100644 --- a/hwilib/devices/trezorlib/transport/__init__.py +++ b/hwilib/devices/trezorlib/transport/__init__.py @@ -30,6 +30,8 @@ DEV_KEEPKEY_WEBUSB = (0x2B24, 0x0002) TREZORS = {DEV_TREZOR1, DEV_TREZOR2, DEV_TREZOR2_BL, DEV_KEEPKEY, DEV_KEEPKEY_WEBUSB} +TREZOR_VENDOR_IDS = {0x534C, 0x1209} +KEEPKEY_VENDOR_IDS = {0x2B24} UDEV_RULES_STR = """ Do you have udev rules installed? From 5481ca36faac7644810bb491160ed57ffa8c54a7 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 18 Oct 2019 11:10:01 -0400 Subject: [PATCH 004/634] Reduce Trezor UDP timeout to 1 second --- hwilib/devices/trezorlib/transport/udp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/trezorlib/transport/udp.py b/hwilib/devices/trezorlib/transport/udp.py index 53a8b0ac9..1195614f2 100644 --- a/hwilib/devices/trezorlib/transport/udp.py +++ b/hwilib/devices/trezorlib/transport/udp.py @@ -85,7 +85,7 @@ def find_by_path(cls, path: str, prefix_search: bool = False) -> "UdpTransport": def open(self) -> None: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.connect(self.device) - self.socket.settimeout(10) + self.socket.settimeout(1) def close(self) -> None: if self.socket is not None: From b3495f76be8a1629ca52863e7678c3d678c4aec0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 18 Oct 2019 21:26:07 -0400 Subject: [PATCH 005/634] Bump version to 1.0.3 and regenerate setup.py --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index a6221b3de..3f6fab60f 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.0.2' +__version__ = '1.0.3' diff --git a/pyproject.toml b/pyproject.toml index 808217d5a..9f2d7e7b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.0.2" +version = "1.0.3" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index 966379724..6972cbbc8 100644 --- a/setup.py +++ b/setup.py @@ -28,11 +28,13 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.0.2', + 'version': '1.0.3', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system) required for operation and development. See `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies.\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | No | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', 'author_email': 'andrew@achow101.com', + 'maintainer': None, + 'maintainer_email': None, 'url': 'https://github.com/bitcoin-core/HWI', 'packages': packages, 'package_data': package_data, From dd4034df2ae2deaeca7f8517186e4d2ed1ceac39 Mon Sep 17 00:00:00 2001 From: Samuel Almeida Date: Sun, 27 Oct 2019 18:24:33 +0100 Subject: [PATCH 006/634] List udev rules for improved readibility --- hwilib/udev/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hwilib/udev/README.md b/hwilib/udev/README.md index aea809b70..57d7a9534 100644 --- a/hwilib/udev/README.md +++ b/hwilib/udev/README.md @@ -3,11 +3,11 @@ This directory contains all of the udev rules for the supported devices as retrieved from vendor websites and repositories. These are necessary for the devices to be reachable on linux environments. -`20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules -`51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules -`51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux -`51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules -`51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules + - `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules + - `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules + - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux + - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules + - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules # Usage From 956f18ef0c31d50e0d8b364e601c2678289a682f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 28 Oct 2019 15:33:21 -0400 Subject: [PATCH 007/634] dbb: Handle string error codes too Some firmware may return the error code as a string rather than an integer, handle those as well. --- hwilib/devices/digitalbitbox.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index bc74a019e..6ce05b2a7 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -51,6 +51,8 @@ 251, # Could not generate key. ] +bad_args.extend([str(x) for x in bad_args]) + device_failures = [ 101, # Please set a password. 107, # Output buffer overflow. @@ -78,11 +80,15 @@ 903, # attempts remain before the device is reset. The next login requires holding the touch button. ] +device_failures.extend([str(x) for x in device_failures]) + cancels = [ 600, # Aborted by user. 601, # Touchbutton timed out. ] +cancels.extend([str(x) for x in cancels]) + ERR_MEM_SETUP = 503 # Device initialization in progress. class DBBError(Exception): @@ -110,7 +116,7 @@ def func(*args, **kwargs): raise DeviceFailureError(e.get_error()) elif e.get_code() in cancels: raise ActionCanceledError(e.get_error()) - elif e.get_code() == ERR_MEM_SETUP: + elif e.get_code() == ERR_MEM_SETUP or e.get_code() == str(ERR_MEM_SETUP): raise DeviceNotReadyError(e.get_error()) return func @@ -513,7 +519,7 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32): def setup_device(self, label='', passphrase=''): # Make sure this is not initialized reply = send_encrypt('{"device" : "info"}', self.password, self.device) - if 'error' not in reply or ('error' in reply and reply['error']['code'] != 101): + if 'error' not in reply or ('error' in reply and (reply['error']['code'] != 101 and reply['error']['code'] != '101')): raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again') # Need a wallet name and backup passphrase @@ -601,7 +607,7 @@ def enumerate(password=''): # Check initialized reply = send_encrypt('{"device" : "info"}', password, client.device) - if 'error' in reply and reply['error']['code'] == 101: + if 'error' in reply and (reply['error']['code'] == 101 or reply['error']['code'] == '101'): d_data['error'] = 'Not initialized' d_data['code'] = DEVICE_NOT_INITIALIZED else: From 6513842447ff9e8c1c55ef5a33e021ef70f9da9c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 29 Oct 2019 15:45:28 -0400 Subject: [PATCH 008/634] Better cleanup of emulators after tests complete --- test/test_coldcard.py | 26 ++++++++++++++------------ test/test_digitalbitbox.py | 2 +- test/test_keepkey.py | 2 +- test/test_trezor.py | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index d2997f582..0013e57f0 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -3,34 +3,36 @@ import argparse import atexit import os +import signal import subprocess import time import unittest from hwilib.cli import process_commands -from hwilib.devices.ckcc.protocol import CCProtocolPacker -from hwilib.devices.ckcc.client import ColdcardDevice from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx def coldcard_test_suite(simulator, rpc, userpass, interface): # Start the Coldcard simulator - subprocess.Popen(['python3', os.path.basename(simulator)], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL) + coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator)], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) # Wait for simulator to be up while True: - enum_res = process_commands(['enumerate']) - found = False - for dev in enum_res: - if dev['type'] == 'coldcard' and 'error' not in dev: - found = True + try: + enum_res = process_commands(['enumerate']) + found = False + for dev in enum_res: + if dev['type'] == 'coldcard' and 'error' not in dev: + found = True + break + if found: break - if found: - break + except: + pass time.sleep(0.5) # Cleanup def cleanup_simulator(): - dev = ColdcardDevice(sn='/tmp/ckcc-simulator.sock') - dev.send_recv(CCProtocolPacker.logout()) + os.killpg(os.getpgid(coldcard_proc.pid), signal.SIGTERM) + os.waitpid(os.getpgid(coldcard_proc.pid), 0) atexit.register(cleanup_simulator) # Coldcard specific management command tests diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 204ffb0af..179ff1ab9 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -28,7 +28,7 @@ def digitalbitbox_test_suite(simulator, rpc, userpass, interface): # Cleanup def cleanup_simulator(): - simulator_proc.kill() + simulator_proc.terminate() simulator_proc.wait() atexit.register(cleanup_simulator) diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 43058f7f1..fdf00b582 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -62,7 +62,7 @@ def start(self): return client def stop(self): - self.emulator_proc.kill() + self.emulator_proc.terminate() self.emulator_proc.wait() # Clean up emulator image diff --git a/test/test_trezor.py b/test/test_trezor.py index d688cf5d6..4a50eee0d 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -64,7 +64,7 @@ def start(self): return client def stop(self): - os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGINT) + os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) os.waitpid(self.emulator_proc.pid, 0) # Clean up emulator image From 8b4494348a70b8a8c9d9099ee7cba881600587ca Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 13 Nov 2019 14:06:22 -0500 Subject: [PATCH 009/634] Change getxpub vectors to also have mnemonics and use mnenmonic for trezor --- test/data/bip32_vectors.json | 390 ++++++++++++++++++++++++++++++----- test/test_trezor.py | 4 +- 2 files changed, 338 insertions(+), 56 deletions(-) diff --git a/test/data/bip32_vectors.json b/test/data/bip32_vectors.json index 79c43c937..816531bf9 100644 --- a/test/data/bip32_vectors.json +++ b/test/data/bip32_vectors.json @@ -1,54 +1,336 @@ -[ - { - "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 +[{"master_xpub": "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "vectors": [{"path": "m/0h", "xpub": "xpub68jrRzQopSUQm76hJ6TNtiJMJfhj38u1X12xCzExrw388hcN443UVnYpswdUkV7vPJ3KayiCdp3Q5E23s4wvkucohVTh7eSstJdBFyn2DMx"}, + {"path": "m/0h/1", "xpub": "xpub6A7PsGUCo9qsp8t5feeCx8AqLJ1w5dECaBAgNjmrhGVPWiPymXbtrBPzbzXVdyHjYxjwbhnM5L3W1368TPXeHkqEszytXPQEk4JjWePv6kT"}, + {"path": "m/0h/1/2h", "xpub": "xpub6BxRX2Zy9Cg6rd9M7a5maB2SPGtctzhrD5HTaqQbHgrQw7mgXHrYNvenb253xoqr2ce64Lwhhfyjd9DuP2AUsE1AmQN9Sy4cTv2ZPypYvWB"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6Dq24c677Ht3MQfNSxPGEC3Uev8gM7ZLZmdTKYqjrJznbNkpmTcU2vYoahnuDKRnuVD63WLJCpzDxnnSQW2tqo2x57aEum6JueVTUCBssPi"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GyTPqD671LbX1J967NaX2WCVhmCcrvW9QBkgUjMSNgXUa1fT7pkaP6SNcNjb3ywSdiG6v951CxrtAseYTrJMr7zoeSvCuFVoFCCbFh9XTe"}, + {"path": "m/0", "xpub": "xpub68jrRzQfUmwSaf5Y37Yd5uwfnMRxiR14M3HBonDr91GB7GKEh7R9Mvu2UeCtbASfXZ9FdNo9FwFx6a37HNXUDiXVQFXuadXmevRBa3y7rL8"}, + {"path": "m/0/2147483647h", "xpub": "xpub6APw4Jtp5eRFbisWf11y1WNpCKzG1Q2f4YKk41fgQukyojzrxrEA1wjbQLWhCgcyXHghV8vBYjTQdVR15Ze7WmR5qWRJeH3gJjjbjTgcJTp"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D4eabteW24dae6vNnvrk1g1PwDiayDaXuESsJRackzADWP7hPiScC42sYgXtxLtKQyWfbNPFv9W7JD6W4WbcM8iDTAgKyRgBq2io25QV6f"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6DvRNUMiiAW57fRscyXwYckXz6BfS25oLMXiVUy9rqNANEkhanmcmqhXPfGHipmQSZwmTJL5tJ7WXmzQCnRDbLPmrELF2i18e7t7jzFnY4T"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6FbZDrzMe2d5uKL6NKxDrc6jpxUtS2MMB25ouXqYdyLMW7NE6ZhrxKhXiB56ucNReFbhFri2XG3ptCUXVuhTrHCUa1QU6KV4z65BLzLcp4D"}], + "xprv": "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu", + "xpub": "xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCRLiTVG3uxBxgKvRgbubRhqSKXnGGb1aoaqLrpMBDrVxga8"}, + {"master_xpub": "xpub6DRjAgkh3vGTWDcEmDp4TPwy48Nu8yrp6swCEdCCLL615CgnZon7r3vXYr8LYibMLJh5DriGSito1FRBwVoBkjD1ZWG4dmgiC935wLj3nQC", + "mnemonic": "legal winner thank year wave sausage worth useful legal winner thank yellow", + "vectors": [{"path": "m/0h", "xpub": "xpub69F7Wq4sNAW3SdJULVAKvemtL7MKkqrWAz8C77TDjGUU7eWtCNNifNFd5odLDZK14NMuZnM1QWmgSx1v44dRCJycFh7JkAbCG4tgxa8aCYL"}, + {"path": "m/0h/1", "xpub": "xpub6BF6djsxYrwkhBEJLDBnTGD2hbVpVGUCrAChbYJEhUXsv5inTEs6NeJ55ND56fWnJtQJfUPfkRYV888FYau7xCd6FaTbkgrab88pWRVfgZR"}, + {"path": "m/0h/1/2h", "xpub": "xpub6DG12XunmXbG2KYRqfHpHz2pUPg1GtKz6uAB6o1Ezk7gtLRhhUiRHtxscXUus6tu42XQGQVdRnT3G5suQDqMcfxooZLPLZJUJpLnS7BmGfk"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DhiedLdjyidqZT7VQSPkhkRMMvuCv68gR85kycWxCUr5F9PTnEssKzRjAbextSmqsnrF9fLG1Y4Stda4oRu1eBZy9x3vznfjPNRjZRSRjJ"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6HCZaRQCgokS56S5Us1wj2NGEbadD9x1qvvwrMK7YKQScqXtwoQ488WUb3sPUYTgG92a9PHhUHGzNarPXWSHADkUkSo7cUk57Leg6c9Vp1a"}, + {"path": "m/0", "xpub": "xpub69F7Wq4j2Vy5EU72B6ECD3V7mJ7ZAENCCr1PX9Huh4xeAgFuEJwywmSXFYiWVBkRt8fcxSawAEvSXYCktx6uysNRzpkbSHsAtiqdPVaMC1k"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AGQpdx1jrUa1JTafcVKDEvC4n9Y2vKw23ucW5kupoY194paKrKqKLYXLetpgcT2tVjBwAtJ1ejYoPXjjZAAKa2mNmioYYDgXuuGyuVUFRH"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6C7BcdKuQZjW86diTf72ApeXAPprujd431tM6psHJXo149VvWGgSiWt95Uonn7AvTWMk2DycXi7UPvniCrM3gBPthyvVSTRrsLScWqNHEmy"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6DboHJhmaasg89i5oit9FeGHS9fgmKyZuLwpjidbk7fn6D6KwR31CPT1HnRbUhmuSsGBBbMGPDYMRs4nFN8oamB1oqAPibCH9iTC7PNzFKh"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6H68PpSLpozG3q1enQtyjaMRBycHyntpHzkT4X1Z4bFj5ZErm6xzgZTehSbmBYUFq2jNs1scVxVTdaGSfFkiUJ6onbnezSkxuaDb91wNUAY"}], + "xprv": "xprv9s21ZrQH143K2x4gnzRB1eZDq92Uuvy9CXbvgQGdvykXZ9mkkot6LBjzDpgaAfvzkuxJe9JKJXQ38VoPutxvACA5MsyoBs5UyQ4HZKGshGs", + "xpub": "xpub661MyMwAqRbcFS99u1xBNnVxPAryKPgzZkXXUngFVKHWRx6uJMCLsz4U56FN7PxTSeVqL8tPJpiCrs1KZh1dV2Bh6QyAbmNmjFRPnkrZP52"}, + {"master_xpub": "xpub6Ckc5ZiE7H6pKsFstoAWHrwwVwCJYnM2B4CzuZNdRvoYQwAZ5bSDQbpqiHp93xKB5UBtqkgJ2sfKr8Qk5ZFWh8cSqMoEtRhpy3LhA9F7x7R", + "mnemonic": "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + "vectors": [{"path": "m/0h", "xpub": "xpub68BiZSLmmvzsJfbCGaXt4pg3xo7wLv6amhfJ1aTiSovaRhabPsMjGkN5NKNYXSLdsptof2xvHwhsGDFL9nBJZrJRpk6JsuesibhT8JQ1NBj"}, + {"path": "m/0h/1", "xpub": "xpub6AzSyuvxXVih9XsNvLm1WJz8ebfEpucgCpiGNGTnA4EA68DFziPfhQjqyVPxapPGd9rCd3witPjpct95omrLu3rBPHjPvkWVxHEZiQUbfV7"}, + {"path": "m/0h/1/2h", "xpub": "xpub6D1MFnN84tjVtxUkgEmvSBo7sRjUZJqfb58981kAMVUMeYRSUY6ET47uS76ov1ydwSGELUj4T8qhyhLnLTxfRy68k41GQaFqheWdaNMscUR"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DYXefDszjJdQE7e1NVkzNxU6k82dViyxJmBj8Exhp4KQDunm9mPnkiGxX8BAUGxsTxSVVrxyGHd6Y8MS6LmYV21NiJFrkbhKzFAZQ2yoHD"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GN8yrQZXy8Pq18XVNK6fMBk74TmhVyFfpbFDzG2ijRjFcRXsMEZSyR3WY1kxmvd3zHr6TrWaK69tHYowenBfB6muuWxZRYDyTNjJTEr116"}, + {"path": "m/0", "xpub": "xpub68BiZSLdSGTu99pyC4ZhP1XdyacF1QTQZBuz5NxY4bkE6MGnBNfVAjg616C2RHrmRS5exA35skNSqcQWvJkH6TNUiXC5BHTPVxHXNmV5tKT"}, + {"path": "m/0/2147483647h", "xpub": "xpub6APBrKxW2PjksG33iyWpMU5QGtLM1VT7VLsdPr2szxat9QL4U5pnEWeT2BtVSb2rmG8iHEekeSjshNMVQvMxGpjsHuT9VrtnUHjoU9ZdeSZ"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D81VTvVgRCFc6FTAhzTEu4BHi3YAVnVirRtjYDkyoqLEqg2RQBYsdTjRQH6tdnaYXCFPj8m7dcSgiwthptYKXy1W1roNoZPAcUG2kqTyss"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6ERnHPxT6n55aQFumEgxKoB5k9BWubR54ZGAo2k9Gr152Upw9tAkZo5mczLBi5D7wZKgnf9TwsP7S7Q484nwWKMRPwK8VhTsWjbSHHqQvWW"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6H2VVMnzdBe61mjbKGsykDniysoQEJTi2gn9Hpah4L4QqZY4JXueF91SLc7psDhv3tDGkfZDoACGrqn5AATsJra8dMse8KaoxTN2x5Fdg4Z"}], + "xprv": "xprv9s21ZrQH143K44Ed4QRTf937zLBakERjDRwqdMirEN9K5GjGedDM4zZSzCTuKSfTRw6b9c6AfNpnLi5ZA6ZWpQ1cmmt86pq8AE2yqeTB6Xm", + "xpub": "xpub661MyMwAqRbcGYK6ARxU2GyrYN259h9aaesSRk8TnhgHx54RCAXbcnsvqVPbTcfcR96ucYohYYsu7j3GrowCcrtQ7EjoUDPGjmj4apw3wCk"}, + {"master_xpub": "xpub6CfuVE8s2cAQijg7nqYKFoEu7AqkAfMNNMufV7utCmDjMjQZwM9RtN9PHxvBK4gkLWRyu8Xs6jh4TwRz8EYiFjWb8bxDMynAwyHZFxwzvkZ", + "mnemonic": "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + "vectors": [{"path": "m/0h", "xpub": "xpub68MXAZN5xcN1p1hWsyDQWztygZ984csLX6AcscDxSt9dthQ9yMyPSToYdJ24jCS5jaVMGSiLeGuP2cWvgKKYQsNXyg988XGGQYgk1FjDv4P"}, + {"path": "m/0h/1", "xpub": "xpub6BKgNwigYsSzHy7igPkEEbXwgnfW2VhxSCeSxLCoQWSpVbTC97u95kQh5Fewvsq1aKV6Y2jg2hYPpPPwnxm1QiCuVnB8pqzPpxbmYWknMHW"}, + {"path": "m/0h/1/2h", "xpub": "xpub6BsKz3uLo3H97eXPQakTxZybNeWjLoc82SYvoJ17qp7XZnnueFcgDftybfSHbUrpmGqFPzUbvaeDWZ64Kr6jbBBWHEqWGQyFpRfH5NzyMWw"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6Dy96x2yusjWMUFQ1MHwRsSisaUTf8vos23agwYus4N3ExDRSps44upCLvXv6qAgN8Wun671qXFvfsxhbwXtNLtr8ck8rgmggyGhYcX2dKN"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GNcnHNdB8BammjG4kKhbWdSFLrWPrHu5rL4bqyFZJXmGNzU4omnYWShdeBHr8mUrG6D6CKJEQCFFzCuCX8jeVx2nYZ7gvNiYz5kdnvLXeV"}, + {"path": "m/0", "xpub": "xpub68MXAZMwcwq3bay2X32y6r7JoqtV6uqtZWKCr3tVyKom1MiyF5vDSiH5UUymFQnBfY3YDgrBAWY8zxM64PBczZbrSUdTvTrCdD46Dai2WFq"}, + {"path": "m/0/2147483647h", "xpub": "xpub69vwAQBFwVLgA5z9Yt9V2Ch85o3C3LSgWCD2GrjRSPqWTsdA7M3qvFG5r4Qas3nYj6ZmgYPjkjy3oNvKavLYz5kF4KhNHSmsqGpmfpjKgqg"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6Cz6mPcGVi3Dcd1et2xRMkReZksdHCJkHp6rjhWZh4TMqjP6RL9GQWDprjk5aUiZ6QmHCA4KniaWXZGqUR8YRZPH1ZNsGFzFsYqM7JFfzSz"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EsdcpaDmDSLzTmt2zJnPa31xpw3w746uncjsw6VFSQfo9hsT6YW65WzCv7zDC6E3RAKe1ukWXzbv232SVvcdYncHbnjTf5cdTNtWRfCxN8"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6FRqFiqh4uk5FP7zMp4aWv2hGbSkcjDMjn1LQrSeMzXBfxz2GjQfpEeaYXM6oi9us3AidGzXehnxuUJyz5oVbugPvbjJ4DSNiB2bfca77Yi"}], + "xprv": "xprv9s21ZrQH143K2PfMvkNViFc1fgumGqBew45JD8SxA59Jc5M66n3diqb92JjvaR61zT9P89Grys12kdtV4EFVo6tMwER7U2hcUmZ9VfMYPLC", + "xpub": "xpub661MyMwAqRbcEsjq2muW5PYkDikFgHuWJGzu1WrZiQgHUsgEeKMtGducsZe1iRsGAGNGDzmWYDM69ya24LMyR7mDhtzqQsc286XEQfM2kkV"}, + {"master_xpub": "xpub6CVHrvPpGM6pXKHRebPy2rzwSz5Nsa7kpQdszzaKJwGBKEP7eZm4EYbw8nNvhfY15Y31LXVzpxGBtmL5GK4PDZ8enSp21buTfm3VH8RPJ8y", + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + "vectors": [{"path": "m/0h", "xpub": "xpub69YDwAs4jk2rtBkvXFjfjauxnbickyq72tByQTmxrmqb1urdstKWCtYXWzRf5ggxEWk758oSUbkY62fCvyd8ZNMgHyVkRjSuEyQEZ8C9JZP"}, + {"path": "m/0h/1", "xpub": "xpub69mwpRPMZmRJCyVSevwpEYtBohmK3BKWvFA6PJqQxSRA9JSYR89wyhsvCyeMh89BoAm3RwHznuQ12Z5nu7BS4jGnn6h5dLZs3LiDBMs6SgP"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CRadSA2fFMaCU5dfbkLqPFbgve9aPmTXaGcqN2JeA51drpatX9xrEj9DPN4NpPf8mrJ991Ah5aqv519ZNK1GcP6a6DJFt1KcvH4DJMuFw5"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EgtSRe3mqSiBFXJuYboCJgLJvzqZvZBiaCCxBTfo8DbhaYBeGvGxyQJEskEEd7EsGMDNuPP28MayK3piaVJKPN14pfRXeexZcPNx6KDRgB"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6H4xNPZa1kMWkPLG6GLpVD9CTwS8KtsrL1PgcYLRhmUFoECvQyt9YYDR7GqJ9jdz6kooCPeu3FBG9mkgN7VcQ3pLmkjMWzHKqVpvRjtRB2u"}, + {"path": "m/0", "xpub": "xpub69YDwArvQ5VtiBG8ZEARjdD5dns3hzDygsbkPVdscZDK4veoshaKsGNsV4Yy1FHMWBXUpdeQPcdAqJVc2snK9AWpKrRaRixQ3gpDbc1KgYg"}, + {"path": "m/0/2147483647h", "xpub": "xpub6BTdMX3yNKnvMqVQUMBaWmwbjTDveo5vYeVJK6irCVVgL1iigyWQCRhksAyw8a7cwqPqjGSzj7RdEJHeoVoWNVE9okYh5Qwobcr9uSSZCht"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6BxAFpunBggt5biYU1oQpbvbtJEuBuwnw1jgAgpUVf39Zg7vg7aTZjFVfu1gUCJ1CCgUQjzS7cZv62pGyFY1gtg5Sh5FaMbbyzMuhxMnV9x"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6F7JAY7Jmo1qShtqWzkF24UsFxoReADUeGsHVG44oDRmNLiQB1j7u3M54MTvtpndA8Mcjp8V2NWDPidHomWiZBsjgxtT2oyve7baR97H2CW"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GectTmvQh1wY1ViMkLLFupp8Td8ovhEesBLZaLSmBPHp8EDcvCczYfqoybxHpUnrxGjQQKJgQZbcNgjY6VgMnuX3XmvqroyF5MqrU4Q4yx"}], + "xprv": "xprv9s21ZrQH143K48UaviLRexBBQSB7EmGa5J9hjUf91xP9RSr2e9BrBZ67ks4jLKuXXSzw1gYXmX9vtbYBcANxztsb2yWCQXYyEBtrv88EXDw", + "xpub": "xpub661MyMwAqRbcGcZ42jsS267uxU1beDzRSX5JXs4kaHv8JFBBBgW6jMQbcA3dnJTGvNS9An4TiW3ibxajo7NKuyNA6AzaFTaQge5cwRuDuWj"}, + {"master_xpub": "xpub6D1rk4onoToBnivyrZ3NNgeP5Ac6xx7HrmcY3f5QvxA8Q8NyUdiaS4wLZgDF7Mt7oFuH9GdWaSGJNg3nrz3GugiGdYGCbadbKAerYERLLyr", + "mnemonic": "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", + "vectors": [{"path": "m/0h", "xpub": "xpub68ieXU9iMD11kBmBkerf8djWzkxqYpqWZ6TMpjWW5nUcWsvYBxQraMyUJ9UPwoFSgdXt6JRYsqu3Ja6jbK3r8s5tqr9vPM6KGrPQRvHut7i"}, + {"path": "m/0h/1", "xpub": "xpub6AuPGgif6dvARqtrFjc54Mnidgnti9ts7PKwUL3rgtxoAEtaNahX2VKmnvUgUJtWqn48J5UV56UnZwVxMPQe4xtHnTaf1ZKpNHe3H9LG3ZF"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Bnt5a7cGnTqLBLjMcDzgiHvwHRHEeXWy8zSGuC6VaXgmAi3ub2v5pUrfEWjDdy7J2KhdCPraJ9MYZ4zpjnMBBw3mgiAkNQz2KDK9dJ8j44"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6FMk3wFnq5956hw9iQBWpZijVwCx5BzWY8iAMa6sdmNPaA18EPDEUYT9dZ7zrQjArgUvSJyqw1cnMySCrMfc2n5NjKBfeyzsY4JL9mQRzaM"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GjyN9p6KoDxnCYCHizMG6phSW68RWESrVjAbZKgiuqezuvXzqAhRU6N6fu8X22fRiE7g6Nemu5PjVirBLNEyKCv2GSi9JxWST9xEr31jCx"}, + {"path": "m/0", "xpub": "xpub68ieXU9a1YU3av8i9x6ir2C25xHApFxeqFRJcWVqsZvwigQiFW9TPu7KU5Gv3K4f9K41qdFEt9ZMZYh1TEebGn4PdSxEtKpoAMqx1XvLgYX"}, + {"path": "m/0/2147483647h", "xpub": "xpub6ARzsJuMKDRpWG9d1Bz2of2sEBF3eq5Jhw5Lx5FxdRHSvda6QVbd63Gch4tprZcW5gP1ArUncL23giQRrVDuv26TFuaKvsoWDrBchzWZ4Qi"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DToMx5kYRPBD5BqhBbbLX1ZxzUSeNiDLMuCzZVJxywutoxyuFyeVfUPmnDbQxgfagDYDsXG8xCycvrEWMtsSoXRTiNkA7stFyDCYWq4gP3"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6E1GdYieSZQcxdVE2TLbuybuSr61XFM77kEVVDYdAZ5wvCZETbxtG8BfuzQnQM8MW6yQZTh9ziRhwoBJej59gRzyvQr5wY1U9ibJd1XSAir"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GM2usoytQ6owMbYhpKQGGK5fchX55HvFxTXVLZDtdjPgaowWndfpvoaF7eh8LTpURog6ZWx6PWRHdQD3THzaTKdferoxyG8MH2asWoXxi7"}], + "xprv": "xprv9s21ZrQH143K4Vgw4mbHCunAxHVRuR1irqSiqXVGpSuca7rczLU2w6hiPkA1HW7GEUZTNtsFJrwggFJZNGRhmRP1e6y8BYH98xMnYDavzB4", + "xpub": "xpub661MyMwAqRbcGymQAo8Ha3iuWKKvJsjaE4NKduttNnSbSvBmXsnHUu2CF3fEkY5W2gsWK3BJbzFWZMK5o8evYXiBxfaKpRS3y1WSy2ViSUM"}, + {"master_xpub": "xpub6DQ4aXnceoS6huw5bff3QGk8dfscDQEYEENkeyWA4mLoTBjGAjjbtEZZvvjuD6SGiwDe6VDYhqYWaopJhacCSJ78w1mmX4GTQ3EjJ8Aumqf", + "mnemonic": "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + "vectors": [{"path": "m/0h", "xpub": "xpub69kYY6qqs9DTh5576CcvDN47G9as43mWYxw72Cx4b5nrrFdh168LaarSwPimYFLeCpBSkYAvdyyPfz1cH7CXe2ibghMkSeaQFdzw2ZBd1VB"}, + {"path": "m/0h/1", "xpub": "xpub6AiUYSn4gb9eN5jq4t5un9DqrEWPU22NkNAiFehFosqdvtXgS44dDoCZBEwZ3Qwnti5fr6kwkTxWxKbX12BgbYivySigUBSEoqjJjTQ2Q4z"}, + {"path": "m/0h/1/2h", "xpub": "xpub6DSRTTZWnJwhrV81gdedgeZeVBuTKVhVdeAG1Hp29R3Dndu6RFCtRq8XtJEevvUbbgL24NcTJrsKCttt7wXnR48J3DG884N1DxzRW1EipYB"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6ETzG65RMvfSzzWT9htP2zsMZaL9KXyixfUVRmjCYtzVJA3Kb8mTHEURiFGw6oHrtUib7UtcYxDtuEsA5KDQeZnsP5y3rMKVFEyUwrumPiC"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6HGtCMuuJgZZ9raLqV9nPWyPppBsNev7bWZgauW9QeiwsiZ2g2hCVjHpDCvecvif43vAawLv62MxMBEJqVquRNv5Q91TTBakFVWATU5cQTG"}, + {"path": "m/0", "xpub": "xpub69kYY6qhXUgVWSMLGB82SGUS53n9vqX4MQcEzrrvEB31DrHk7wuJ3n3q8aCoZx9Qd96ji7os5UrxT1uoh12Ji7fvMeCL71hdVn3vqHFvpQL"}, + {"path": "m/0/2147483647h", "xpub": "xpub69xpXiFXyY33NKP7epeRqdHtdw3KXeQnUPy1LdSonFGdhXP7P7s6u9XhJpkRbWMfM3gk6r89DFxyQuTfuWgG98LW4S871o87CKYQ4rTrqwd"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DSUQysV4JdrzTj4bMdBgZDwfLDsJbH4XWVXT6boNSK74fUbfmsvHMNYjwWqJSVcih5AaA4nKDN7urYxKsLdG63dCqgYyXcMgqrPnyieL4u"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EDUfvX98yhnev1QaXfryJemwu6NKQjFBx1qv1D6AEG9jP8HUshudjtgBTiA1BtjX8Er4Yayhvz84dhPqZKubmovjkVo84N76go31uDGtTy"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GHwhtrc3aR8ebmYYbV4YuekQd442zwLkY6tH2gg9TNVQT2GeAVhLZTz3vbAkxu25nUbt3fdXb8BMTnaQxc348uaP15sTCTVeWG6y6ZNdHe"}], + "xprv": "xprv9s21ZrQH143K2Z2MAexszKEd4vm8bHh9vdxreb2KFq2deUv57Xd8winRtRaRFAvbixr43BhviFF9mSQ2TyGHuwKKRAmeVc17wxFKEbrvKh2", + "xpub": "xpub661MyMwAqRbcF36pGgVtMTBMcxbczkR1HrtTSyRvpAZcXHFDf4wPVX6ujiC1qwz7iG4pUpLy3FMh7H2oVQLTqdNSQjzxhBnJgVdrn5mXP7j"}, + {"master_xpub": "xpub6Ce9N25NJgK748emHH27bLsgnUMmmfFkGchDoZvNedp99M3JuSb5TEGZVt2Q2pYGCWGQf9qYaEeChbnNktuRBGUT6LoAGX2DquuNdh6uK6x", + "mnemonic": "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", + "vectors": [{"path": "m/0h", "xpub": "xpub69PxTAMrS9njGezy6bQzTCx9kY5d6gyW6ZGJF79vn8yaJZevmF3ZGZZREG3thR8YKZPVkPBCr8nemnrNZ4J3RdNhCAQVEBuNRfMoLXB5gin"}, + {"path": "m/0h/1", "xpub": "xpub6AMf3EGnY7NjjP8TsZb4crqPcU9QnjDG4btba5j9KuW7uUTrQxQcYiQHzUMEmZ1WSNqvpfiBymDd1CvGgWfLePjffBvSJzapXaiXJWfkdm3"}, + {"path": "m/0h/1/2h", "xpub": "xpub6DJMsJZRTsu9uGjTu7vGAmVFzFeK7DYVt4dU2FGH49LdcWNtfLEAFdaSpYhfnStEKDCi6fUTgXikdngQUWMaYADrm3eL6P6hjzkBrNZUNm4"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EWpdKAu45CcF3KGaTJZrztfUgHpuHepGdJaHsRmmXZHeN6zkn26gPavmMazW6mQmmv9PSV3iBB4Mk2TQim83FBZFzcxD5tTZ4qVZbTA4YL"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6H81njH6LSoN4DKqDEdqTaeG6zdtQR8K7XyeaaBzvJhH9Dd13AUxnnj4hfT23TJCxD9FjKATA5CS8uqmEgif63ihMPvP2GxT3EDpZHDcAXY"}, + {"path": "m/0", "xpub": "xpub69PxTAMi6VFm4uyeW4AsFRubqxpdz6gdiAUQgC3Q3AJweNQRrm3whTe4RZENEipuNCVBUgGAGt8TLvvqHCtgMEjuH5BNXV4fsVDf5VtrCuA"}, + {"path": "m/0/2147483647h", "xpub": "xpub6BGLDPDW9yuAt6HSUzrmFU5QmQgHb1vfisnFZ1YQSLppPry46RajmbXw8rhQdE1J38ATadeX6HizX1bG7z1BxTYrpA6TYWfKxtMyZ3U6T9v"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D9U8wUwhgexaN99MZBn3EafCPMSoF4VWDJjA42Qn2fWEHa3VugRP17JfCH8TyusMMzh2nbYUf61gqSgtEijDhar9yNZDRGQR9h5eYZgU5K"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EGfzg4fA5qPwxkAoqQM46b6wwRJpSMGUfz89rvgbnsRUSsThMdH42ovvnrxSJac4R2PYTyNvqz7HW4smMnwErZ1BGq81a9jhLwxV1GCh4D"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GCvi6wxjvTSYVRx65AqLrqdmdVFk6t4ndoxZKfspwmg3d1vccNe8AU4o3V4AaBiFkz83Mxq9nkrJkMRn5nkdiSUepUfsU9kmvafTcx7GJM"}], + "xprv": "xprv9s21ZrQH143K4MVHZvjndvSoH1tHuM6Uf6BTiPMX9Ni2Dj5iqxtMGycnUi9T9tUUckPJnuwGrRf3MVVD5cvSF4svqfHNdKg1SSkvjSMBgxb", + "xpub": "xpub661MyMwAqRbcGqZkfxGo14PXq3inJopL2K74Wmm8hiF16XQsPWCbpmwGKyn3JpCmsQZNUBH5Bf5x5np3SX39PtxuQ1GrQvo1o6H2y3g29MT"}, + {"master_xpub": "xpub6CDwootAjK1YycduSmTrAAXjfW9A8bPhVamZeofd8wX6rvGm2vLz6qtnqx4FagbFeXJwFThkzDPGkErrjFpLpr1wsj7NXgHHkevPUxHYjUP", + "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "vectors": [{"path": "m/0h", "xpub": "xpub68WQ7UiFxwyyJahGvBZ1cffn8vMHn6Vti4fb62GSmErn17B8SJHqJPjAdTwbECo864SjV6PJVqYoAuNf4hYaTebBWZqjxZV7LR1r4aVkPsU"}, + {"path": "m/0h/1", "xpub": "xpub69wDXVVjVqKxuC3nP35gtBVh3VxH9gDKTbS15vqyegs4quxRdVVUopA5E7C96Kpdx5dTeP7CreqYhPR9uCKMvwXykDwt4duPJjDiSZgsAL5"}, + {"path": "m/0h/1/2h", "xpub": "xpub6DPsSwT5tTazDN8bMHseCxBAJE5iVvU5U3trCa9mdXiCmDmuAW9sKeJWGmhmsWchzCCHTUahMcG3Qyrz7SUbJzs9tXuZagq4btoctF43Tae"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EKnpBv8UJeJiqWWYaRKYjz3R6DT2X8Q18JurLXY4gqHqMpdRK7ghQWkg77Sv1bJT7gpHhMssu7uyUWx1iFtQWGZjqBta3xqhQ45QiDHQaM"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FR3eUHArz4PWkHuchTK819Eaht2QYPQyTcETBrrtMoCTQGm9oC38YgpncvkPHHobQ5KLLJa6GP6AcaYbkVGK49RRaecjxEdVHnqgYnu6Nr"}, + {"path": "m/0", "xpub": "xpub68WQ7Ui7dHT181UsjwnViF47KcBN6cVToNcW3fpWzqZYq38UEAZcSmB9BBmSMLSQmx7NthScekEfksmo5ycduFLKTzjiQFz7FyEKTb2JGfx"}, + {"path": "m/0/2147483647h", "xpub": "xpub6APRMNXW9vXympmgVsbXCaVknpBB41oiZ6mqJvHHbKB7yi57FYXXkNcf8q3Jqjctnzz5JJPgFPSm92QXtSAAwQWC5XjXksUy9JDuZTs9ZdP"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D3kpdeFpzS3GEPNwFQuPtaisUe8JNMNfQJ6141Kt6KaEnRS46pA36CXWQi7u4Vcmrkij5mP5gFvLzWnUxWLRYxjiw8YSzokLwLLoyR2z46"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EmF1TYv3orHinGZnV1FPEySvrNdZdZnk2Qwqfvqh58vgtWaUVCNFSiEi77QEHkvy9tKZdV6Yk3SxB3g9kMAcCB1fpNGXEU2avYpWoz2iEL"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GbiWsPRyHpiZPGDDb4YiraiBSroQuiEER47exQXiLhtRftWRDAnYGLGLuhP18Jrhp3ENnAXk64WWL4DWpsJdRsN44UTuoKRNjTgYucz4tR"}], + "xprv": "xprv9s21ZrQH143K4VHfAaPWRTm4aoHAZhJHunsZZTQptR82FSTZRjBGXBP8kQKHrUVUE8vMM2Z3h7UoG9x9XCt9FHQ1t1nHU7zQDqrEszAg28q", + "xpub": "xpub661MyMwAqRbcGyN8GbvWnbho8q7eyA29H1oAMqpSSkf18EnhyGVX4yhcbfjXa8j9KW7APXiBmXXpfseCZy4whWaFo1xsQoTxLPYJH17sBeA"}, + {"master_xpub": "xpub6CAzShWx2MuZwVxHdXQhSbyGb3DM6vLeNeqNVqFJBpbioYB2NytmBPSL4BKWpvSnKz2T36obQMghidDz4MnicBTuecFTiSQ1CJM9xkfFiAw", + "mnemonic": "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + "vectors": [{"path": "m/0h", "xpub": "xpub67zHQDXa7ubWKhyALcRGfe1P6w1w2eJbLLuULbEJiouPBjm7t2JxJzbXzyf3YyEtuN5PD6wDTWse7s2nEKV8pKUK52Kd3Hi8CjjQAadycsp"}, + {"path": "m/0h/1", "xpub": "xpub69rqrQkTqAodF1UbKhULUgze3bNJ5sMvZxiwhFFrbxRKWxxR6oVbXG8f2fWZCSxhhQeACMugNstEng2W6FMhFrTwa5H9pPa3GJkxQZTGDTa"}, + {"path": "m/0h/1/2h", "xpub": "xpub6DVfjXkXtWaLKdj5KMi23W5zcNooeEYKCPdUWyCQ1pSjxwPLYLv6mMu48xaYU7PSEYr2b6EmBZg6PeuSbxZZbyzc21wAfVZGrfjHtPiEznf"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EEfUFTFfMn6a7iR72gyFwiFaeibasdWPVFo8iie9GkiaRXYtfhZKDbtzD2aCDNoaPTDcwkp88Ry9LTZU42CXw8hd32DkPxpCTdLcjsyws2"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FvEpkg5gL3CFMTjJytEc8cqai4ohXqgzY7WATvPn7kyLXEQqugrQP3dTQxMA947vJ9uC4ZLENKPnfEuAhSy46zSdwQu9zEBSsCPas1coiP"}, + {"path": "m/0", "xpub": "xpub67zHQDXRnF4YAJJmT45Pz8a7vAkfrJxvfDSjzQYeijn1jcBKE1pDtEDW8guidCJoAsSkH7tvy4kJUezKvyEJJrsrAyXTjJwjK8dSJE8BaR6"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AgcuGdNhgvettDobCw91mASBnnMZj2GHf1aCxUknhcnc7QWTaig4qMZHZAS3j5nUQ19EU6sQN5oKY491Yq8yNtockuRPT4vTHszSfZHneR"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CQUHifvdxd2wCCtNrGAsUETFUuVhBWvFGkoJKAYNFaNPWRT4NdA5Antc35bwftPa6Kndp2eEweERcgYKzei3GL9hSmGSCjVuZjkzBrntrm"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6Ex9nqA2k9wjpzSBRd1cp8NTzmTWkJ8AupdcaAuSH9GZ9m9KcN5SnXgVg9hzYMgUm9rHV9qvyST8kw3RKfanAE4FUutp6RKS9xJduznWg8J"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6H7zAFbUB4bssAQJeGsyhcx7mQvsYE6wkt3nHVGreLeY6dqPsEUzLhDmnKSHAcuD2giVut1wYPHiZdRast2BA67X86i9bk25Sb2ZNtmk2Yw"}], + "xprv": "xprv9s21ZrQH143K4Az8C4dShiQm8vTkWHw4M1FmzeW7zJWWiXGbXbt4AFCMnPpoZL3r1MbMJX269yKRzd1uRb6j8Vxq8BFiCWssGtspW7Yxz7J", + "xpub": "xpub661MyMwAqRbcGf4bJ6AT4rMVgxJEukeuiEBNo2ujYe3VbKbk59CJi3WqdeEtrKeMeaZUa8sGnC3bDSia2hUTuwTamHvJe5Avm6fYp1EUwJo"}, + {"master_xpub": "xpub6CyF2xxfekkigMca7q3WPBzxGwbvapFgL4sqFPiQDPUokukwQwGfDxUhS6sdqs8byESa3iJkosDgc5JqzkkXX23smnuJ8w2q3yzbWWscxqk", + "mnemonic": "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + "vectors": [{"path": "m/0h", "xpub": "xpub68cCaJLdRwjMfKDSB8DEQKucKBEM6jSHSGmcZpHkbBoLrdpRxvoxefqkitVNV2yoXzzMrmsMdJHR2LTKk6DHfe2WkLLR611PKvXGv7oqa1L"}, + {"path": "m/0h/1", "xpub": "xpub6BCBqUcUBhJfhJ4pSAngJFKXCHq1PvixKm7saV9B3mwRkpw3ndbDAwyrYwX5QrS7ZdAmRg83qddJtoXz4ZfbTQjwQPEpHtEW2RLG3ood7WH"}, + {"path": "m/0h/1/2h", "xpub": "xpub6BnHwvrJ4FQG63FsD3f5uHd8RU6WQJGb3W8USKxcUCiy3P3j2J6Mfvjovx5hFrDdPj8WZWcF26CwB9UkvLwgbJosGuCySHMweVejbvZjPLP"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DuRBCr26G5sa6ENyFUfzKy8ZzR22Nxyka7oXm9g6jy3iMfVxEa2MRXg638uXJY6RgGBKCGH17rRgVSwCXsvVaqis6pSsjVvuKiqoKEhoK1"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FjeyDpVqLome5KYaVio8LCZPEZo1NjEDcE8agAwSUsmH62h2oJKuxc38iYaRCRYmqbtUDtMm5tSYM5ew77GxoGwxsFn5K2n3MKUZyGEJ7o"}, + {"path": "m/0", "xpub": "xpub68cCaJLV6HCPU98ycKUHUhXw6x1eLg1phTUCiq5aWJk9R5QABVXqYyM2uap7AETiMdkKRQ43FrHgtWytpjWpPjyW8F1xdtNQaWEHURfqMBU"}, + {"path": "m/0/2147483647h", "xpub": "xpub69qnUJVJ4g2xN6odY65QWZP5J1DtjLFJecSG2EsQXZxDok6tRFZKKT7GuwaAvudzaVsW1jPUQ1KgJmTYSvbxHcvr3icsKY2Jd8v6SqrqsEb"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CBLRYToDorih1aN21kREiJtg3Nj8a7qcYPYNuQa52hWhijXCPvRH6d6WE8BcNxAiY6pywDA5sqQ9T4ipZJ3tkczGqgcZpAayPLpxdbG4qc"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6FLsMnQwYDW5duWWAdP3ZudUniGmUn7irqQPJwcRBaTxi7Xc2E2YvxknMW1BuGnDXSvrBZeWfhDfTF8MPLLma2cheGKipKjZAH8odve7GVk"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GbAenqCmDbxi9ga8ondQ8nhPRykwmAbLQPHoR3AQU3T4Cqb3Ap7oDbcgtTEMbSXJbwbNqUgwD87Q4soz1JV7uefYxDr2SqRank2QhTLP1e"}], + "xprv": "xprv9s21ZrQH143K3iJNbWM7JeraBZf6a4zC99owVcZKFRAq6kVKcpg2q29TXcpMeiyxSRkNwFxGdku1A5TmWZMr71Dp6rs4NYPwvVZWJmnhXZQ", + "xpub": "xpub661MyMwAqRbcGCNqhXt7fnoJjbVayXi3WNjYHzxvokhoyYpUAMzHNpTwNrGY2Xm2eKBDzVoiEWLkz1FyvrJ4XWtSg8qxAedywhuWEaMwzM5"}, + {"master_xpub": "xpub6CNmKkwmGwGoxCCfMWLS7dEvmouvs7i9HjxhTaYbo3Yrjhevrvr5FHDzbwKaYorWJP9JpnYS3wREtgXD6gxqmJT2KDXj32ynyqawCRMqBRo", + "mnemonic": "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + "vectors": [{"path": "m/0h", "xpub": "xpub689yKeTwX2tatRMe2zqpWsHp2h4Eaj65BThH7o3mnRJ8kP97VgRjm1Kx6vvnd54RG3Xy4z9iA24U8gh9dNRGdki5gg9LZM2XCau5W6xQpEG"}, + {"path": "m/0h/1", "xpub": "xpub6BMxSsC8ztda7dBaDshtNC3DWM251H7UJZFKrVsEeBQ3crtnmgdYuVzwc4Eut19HEBgmG5afN33KH2Yi74C2xHDqdw3WvbG8MAnK4xzZLhV"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Bz8U5aS7thg7ittXpYKCzJWqgNRNe5N6pbByaemnmXjF7Nc2mqoXNtr2dKvCcMYkMUJozgAat2EdvjaEgwsA5sHQtmq5XpYM5VZTWSihwE"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6ExhJFrqEgJQauXWh2tyPutVTnQdnvfQC92AfDTDTHV48N48S1bKs65qmifyAXoASSCX5WgrG3voDHWohfmVied1EEde8kmteYxBRGSc8r5"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6HHZwQuCC9dKoNhUrdgUsCUCRXb8qyzhBEeuuck6mBCaL23wZbS1qnmcjjqr4Y7TPLEWz9Zjvb9M4LzqAJ8QikZpRCmkBZ6U4mVLSpKrqTe"}, + {"path": "m/0", "xpub": "xpub689yKeToBNMciR8vWQrPjoxjLRaugSqvF56BTLt2Q3VFzsHUiRGC3TXbziMWf9m1pxKL7SnXfdDqLejk8w3YPVpFuJxBr4n6k3eeVfVcpnu"}, + {"path": "m/0/2147483647h", "xpub": "xpub6BCxyodJpNRqGKbFboQiDYRAx6yB8XX4rSv1NngsNKDCtfqG5hviDSw7nopUTReebJV5F2vo4eVEtSgoy8ExgBGsbYCegCPVHpvL5wous4J"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CK3i9KFfaxuhY2T2wtsrEfhes1DWxLaJZe7Ts6qE6nD73G8QxC6o4emwWbbugbeyk4fmYVToVTQ9hY15npZU83MgKVAr6iRDBLXy2CQGQQ"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6DgQSMt8yURyZsGRXVAofcbCpYgMAXmTDwdhMP7sSqeD8nbo6tzCEL1sfsnRjvt7u4WnPAuUsMDH4exswqz4PuwE7JFyQtVe3LdyFSga5pL"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6FZRf7y9VTYGLoJToAxVYBsK3GP4PbDKxFqXPe1ps2BHZWj2tnVHb5twZH9GYcnP99BsSohEs9z9cVgvNg5hZBbMyJjwan4spZ4Jn9Kt4wT"}], + "xprv": "xprv9s21ZrQH143K2GsJn6WxP71Mnxxf4X48Sjwzyb6KqjGzp3CyHaQxEFjdukez1nhkf6U6zc4GgJVHUHJ5Jn2F8JLCdQCnjSS8XwMDqm4npwa", + "xpub": "xpub661MyMwAqRbcEkwmt83xkEx6Lzo9TymyoxsbmyVwQ4oygqY7q7jCn447m2kp2JWAayzQLTSLUpSRdTWsTexdRM6bw6ufMHHCfxBMP6dcFrQ"}, + {"master_xpub": "xpub6DLxm1owyvTGWvffXks1PsLWvSRG8DiBEQLySoajwst7CVReP25eJZ7DkMX7mQxuntT8RPaRxccnoQWSm6PMJW5XZoWpnqeGJ4w9KsvzjdT", + "mnemonic": "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + "vectors": [{"path": "m/0h", "xpub": "xpub694v6EJvGZgd6JE6pAamYSEGP74pbnkWTRscaA8gie2EmnnPTjhxPx8XuNnGsFcZoER58YfjGKhLbdYfB8MSWJmquRAosmXhgFbdSXTRYkh"}, + {"path": "m/0h/1", "xpub": "xpub6Axtfr9F1d2scttMN4Er7qdRJMwQ4zWm6USc5njAM3upxJc8DYEJPMvo5zCoEG1iBtaDDNVXJgnEKDBsk96pff4LXAbG4JfSSbLwLqq2rwH"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CutqkSAAWUFGx8KBF1WmVVZbjEVyFBD9VmKk5hiREdKfztQVwN5pDjJWyxJRSiZkXoCMh7wF4PSeGLqfarFJ5UioDFjb7H1g8fymWNR7ma"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6FBJGp4h5teU4sP9Zv2tqTiLpA3N2t2iN5WKoY8CYHHiy91P6V3UPfrUdRPHFpaQ7ZrB25w9Npqhh9t2M7QAR3tG3pLz6jSKxyQQiwBTiMd"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FsrxCEXju6dPEpVwWTzTpESR9GhNERi3wpcqcWTm6rkUMikiuSV99U93uXX9okNmBvf6e9UWCVCcdtbv2JyovqzKdJ3X5i4BrFbCpDhtBJ"}, + {"path": "m/0", "xpub": "xpub694v6EJmvu9euRDUzuTxnSrhcYg1jn8zNS9FqgLMVqPLAZCkjhtKAnhfcqbjhU21FB8CGEKDFjqmHonCH5d9pja4jBFUwyk4ZwEjE8YBihb"}, + {"path": "m/0/2147483647h", "xpub": "xpub6BEAciQZRVAZD6cfkUTpJ9s81rxEVY9jSunrReUUA2kFuu4RoELX41pyvW9YeuqfsDP855QSawJZvd3bNMpVUvRUGyeqRsnxt6DbUi3dKDE"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CTijsNJuFFWveG8n8FVM5RSzbQMgxBN5HDqXgWox4rNQEv18GZNBmLJUrbwKGiJZxaPKr39MiUvbb8vhsGPLXwyRfuX6SAFS3HgA7KxRPU"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6E4dpYDTXPpn5X5j3wmgX8ASu6RHYuoHfzqMGrLPYa2NvpJQZywURJqpzGwaJkj8EscK7UmN2mCw74zYJSU2YiiJ3SjTPe8hYEGKABmjHtk"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GmwSYecVubnsoWLUJuwu5vHfkqsVqGcr6MdEWfoTHhmLeK5xzyHLKfG8HiaJGD6cpRhmz569bUvHeuvmCvugc1CTpQPddvuruJgp3QeHdV"}], + "xprv": "xprv9s21ZrQH143K2vwt95G9vjWpa1o7izT4HqJFiKSdSeUAaYGz6zfW28ipAPB5eJqKccqFHU2BxGCvhQkVY7hMRdwDrHvsqe1GbkMCWPB9GGm", + "xpub": "xpub661MyMwAqRbcFR2MF6oAHsTZ83dc8TAuf4DrWhrEzz19TLc8eXykZw3J1gnvht82BHy8cuQWBz1HMxz9qUwLLRKH8BvpdJ1EZFZFAvzgRT3"}, + {"master_xpub": "xpub6DTf6J4bPq9gLPAqGBH7fD3yPEVdiQkbdu8husPVJHYed2AqPfBGkdbG3tuTQcrgCmYG88gcLxQ4WqpPgywtmJ5T1rSTWKGvJBjxuSx8zfy", + "mnemonic": "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", + "vectors": [{"path": "m/0h", "xpub": "xpub68sytstXdaAtSuvFAXZZYx2TmDHbaUdZaQTi4NBqCXz4e9hwe9fc3HgC2MWH8H6RFkDgt1v2kmow2oUXpTgXpba8sanfarn2g7PNA6cwbKZ"}, + {"path": "m/0h/1", "xpub": "xpub6B4gDtfxymZfxjCH7sr6AkGT93LkP5bStfx7y1pM4yCmNeR9wCvoRn2hJzzzBboL4Guj3D6QgFJgEinVXnzSe3jysszwYBVz2QhPGoyYmuQ"}, + {"path": "m/0h/1/2h", "xpub": "xpub6BwdYPgmtNfwd71AypQJgqkSxRVvRp6yyDZZMjsi7bU8CB2mFbny5eP844C8Jf8aGdJsggoPUFH3RFjLdjyzrLahsi6fy8D2BMMA1uXGWeQ"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EJ82W5gfyjbzmqXPczXizqM8xkA68vjqq4nFy9DitsKjk1gHX9kpscXLThjwpmM4TBJNX2yoV8uBMYoZ8i4v7VXHDDrSZCZTyKRer2m1Dh"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FriRDVikCXnDtNpyQUokFo7KT1yvwtjpXTRXzdVNsLEZK21T8kEAmFsHfFY3Yv5VDG8zNU8WxdixLaws639wg6dt64To1imRDMfuk2r7Jz"}, + {"path": "m/0", "xpub": "xpub68sytstPHudvGMjchVVaVr7p9WQR9gzNUyqRCcYqSo6dNKrPKGC4hCshcdevitcjshCS2pzrP86E7crfarwC7h5EtriB3cfpQTfg1FrHStD"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AE9fy9Hbu13beBiHdbvdfFU2MnmTWbNMHiZrCaAzz5tffYZywCvwFvNHxPbT8aZWPs3mZ5vAx6PphKfC5wDNbmd63EVk3KA1QGEVGMZ84B"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CFpQ9g2sVsj7bVsEkVjKXYF7ZPwyYjzxvf75WRNwgTJHLh9aMUVwPbFGmfShiTig9xVixn8qZMpwqme5PE4TAHXiHApXCWWQkXNs4mjb6R"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6F3QyGYTUJkK2cP6YkRQ7hpXBPrKX6ysHZtj97t18SSpFyF3cUB9tqErjJPRGL84g7BZYNBAK9MTW8MRhqZ1yo9K9F59wfVAMXF9gXUeEqc"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6HH1bEMC3ZiESZkZRE3vvpqmSsc44LaA29GULMf9HJX44FGKKpxqKXhfAEBfHdTKxU5rJXWvPuKFZi6E57GgZhuw28dx69ZkNxvxT12Bx4U"}], + "xprv": "xprv9s21ZrQH143K3iQQGmeRFG1W51Z3Ay5Rre84yapj8yuFRJYDUfD5HBWTFqV3p1kgkHR2u8opVj8AtZtgm6SBLnSLXiAAaUfcR7zjSj59eT3", + "xpub": "xpub661MyMwAqRbcGCUsNoBRcPxEd3PXaRoHDs3fmyELhKSEJ6sN2CXKpypw78TDYZrDDuXS72QXFLz33cxXMfaX95zoKdLGjjFkzj3erDT2gCd"}, + {"master_xpub": "xpub6DG4hM28j884yDYHGFU7G1omzrsszg2kqZ254MDDQ3Q6Kuwqtvbr7iJK4eXxEvHattZ4psWJVMVjyCo89ySNtfPfJdHhP7gQu4hWCZJyumk", + "mnemonic": "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", + "vectors": [{"path": "m/0h", "xpub": "xpub68S7hrU38ptexjCsHhEuzT9oFB5zwoUXkk7s6RXqWnLxBeUqv9qgPV1ecdu2Du1t3LEKr7DoRFvwD2G5CHxQDBsA9zejWZQcewkhBsb2mHD"}, + {"path": "m/0h/1", "xpub": "xpub69tSRVcgC3FGL7SSWHWizAWE8mtNNJeCSqFi7pPVs8LpnG9zmias6yKgJ3sBmouD2CWh3ccnWQ8KAgpfDqD8mxSp5XzimvwUU6CSMfsbU8P"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CwCqYcgG2kJyq5cUDzKH6K2J2Pah1P7oWqtpLJweHiN8oqt9mcbGNhmWsAnFDCxbRneJgJHz3z8qiPCS53vm8umPfFAEguBzY95dEiFirT"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DYKYivYmAsUmQUG1dsLrwxv7Qt8wwXfMUd994m1w7UWQb8gDZhNn3MKYchYxXHtiS4suXQGLog6AfXQXF2X35dpvgBNhmdvZTLLp6uWRiq"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GTGKqBj1ShFYsWNfmekv7RT2yxgjGL17BMyGYDvbKtJVgh8aJJ2KriWTTRL17VF2MDaz4dRDA1ozFjb4uLARE8KpkhNFwryMtqZkvg9hXA"}, + {"path": "m/0", "xpub": "xpub68S7hrTtoAMgmWwGTmV3sGh5FW1CAUTPEKZw4PticWtttty9MJydSY7vZdLLiF7v7BxCMi8xeqVNH2ccdVYYEkHcethLuUbck8ktt2sy25E"}, + {"path": "m/0/2147483647h", "xpub": "xpub6Be6KtuzpE7iN49NdVdTPMP16j8S7s8KmSuZPC6pepX8tDL1RFyFxxnmXi2u7iELDCgDsHbcV7fUWBVbaWTMQLXDdEm1cLF8ENLsfNf3xdx"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D17UtussxBXWFUcfSWifGMnfn4VF8dpD2svPzkVNRGC4tDaJSodAcsgdK1WAQi7QMxMWdURFQZmbBKKv4Ktze2vSVXaX8mF3NruhR3Qfij"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6FQmyux3ETbNSXTHLYDNGrkF58e99AiXaFJag5rDxAzR9V99WsW94krCpuAAdQBrrDwdZwHyACFcpm2RRQnSfzhkVbyAydxwuxPRoqD7A2x"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GyS9Kpxgbk1FfWaEdrzYuPkrzRX92CvAg1WD478CjfVCEpCNG9jUumq1TVyNrt51Ji68rc5hxPNcdxVYHshDqewMnRDfD5emuTxF43XcPG"}], + "xprv": "xprv9s21ZrQH143K2GkCPw791LqAr4t86cdAMr5KBLZpYFJHVwwfGaJFw9SZmcwB1yRa4frhhNUGhz3LXJeRVjPamiN4o25ij4tmwVBWXtA6MTx", + "xpub": "xpub661MyMwAqRbcEkpfVxe9NUmuQ6icW5M1j4zuyiyS6aqGNkGop7cWUwm3cwMCKRSyDj4vYcPREc6bpXc8R4zoBa2wkF7MEdEUa8EUQzWeMNc"}, + {"master_xpub": "xpub6BnoewRSzkrESpkG9hYgxGNrcj9cUEMC3pFmuzRzfUiWsAgzdsgXK7LgtR9e3XzY2Y4dXHy1o2mejaGHDkhjucRZNQb2gG376ST8D9ffDhb", + "mnemonic": "scheme spot photo card baby mountain device kick cradle pact join borrow", + "vectors": [{"path": "m/0h", "xpub": "xpub69Eg2d9znKavWXBbsGpTYRZLfjEjba9M6f1UamqN7B6XC9t9tnes5zN77LzUQje5dCNCFbuUn7edmvnPTLgrCx72mFqPWPsLy7hwQqyP3UT"}, + {"path": "m/0h/1", "xpub": "xpub6A8KKcvTKEDQTQgY9XMhjAvCNBDx6nSkE71VXgaJZPnXFNUX6ELp8X8PYuwmmP83mTNMfMMHZko7TCcaUot94dx3MfgwjdUhsf9GzucP9Tv"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CaNpwAB8nx8XAr5kveCzAqkd4hRT2oXcNVycgoZfprJWM5jjygYEaSTtK3ASZfBENYPrKY481To7J9cUri96FES9N9BTn9TN9fHZkTJ3DZ"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DfikXpTp7iTHPYXrGeCMqrorH4U4urRzmVwmb3jLDsLbywPw2vP21AdE7cqQM8gGFjc2oVnVb6JTAUsZ8DHMdXCYX7sMef4J1cz2gtMQE6"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6HELrtBuV9wp8qFp85jLxVg67FZf1ncSmwYqifBWt2oJPW8AtQaJpfmipmkLwv1jJViDfx3mJupj6oyjcK3xZMDQXgcxJGn8rwSkYzBQoJz"}, + {"path": "m/0", "xpub": "xpub69Eg2d9rSf3xM2wWpKdNLfoeTfp6SGUVFWqQPiZjiwabcdta1VTeZi8wju7HgGq2jykoF35G1sWo6GTEpKEWrEiJZniywdJj4dsSVeou616"}, + {"path": "m/0/2147483647h", "xpub": "xpub6ARap7ALftcWLUtkhUTw3dZzig9QmoHN95pW2NPnno8ayMNiy6tcskkiG9cmQviE2MfZf55EbAck34KQYohdPggQx1u3q1Pk6vsYhoQSd2X"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DRsUpAkN8r3wKqz98tp9fohKnBHS6yWecB8Nq2VexnWcP23qdaP8Nh51VTYH8jeq1vZmjBDV7uxSMvBeyd8sz41rHigdboPUZX51NtYKFs"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6Di9ARPQCsdBYnDQRGaENtT7Pr1L9furawbookQroSEWu4j94MAcpp63jLhn8YMGhgo1sDih5ToDWyiLsepnG8ZB2xSzdj43zfnxyyP3rt1"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6FZBbkq9qkM73TjP8jihhBtW2PZYcfCnSCNiZnWVGSE9qrTSAx18ka5ZNYHT4DcX8tyRbVxGpFY52kndeXr2ofV1gE5qL5NYYvTrmFguyQm"}], + "xprv": "xprv9s21ZrQH143K2suBbFp7kn3RMQmGZjUqePpEd5Byx1DqFJRWNdqFrXMJGRcBtxu9zYw974W3d8YbHUPGFfnFghaY3cgXMM1bgkqrSpUpnFy", + "xpub": "xpub661MyMwAqRbcFMyehHM87uz9uSbkyCCh1cjqRTbbWLkp86kevB9WQKfn7jPQ1GtP67BL5CsFsVviprV4j1rcWJhADEZ8dTwFssydWEpi1vn"}, + {"master_xpub": "xpub6D1NEHYCQwSkVgiQbChnrNH2HKrzNExP9coHogfU3wejAJCt2FpmaYu7RgZwWd5ZqX2L7AfDByKdVRP4opc4D6nFrsr1k84uz144GtMsy5H", + "mnemonic": "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", + "vectors": [{"path": "m/0h", "xpub": "xpub68c3ytKDVQAAGGPyiLwGHH5b5FTetouwKvmET3HVxywFLr91UfdZvswJYNbNnKfMdKb7CH5uoPV6Kew9rExKhSh5ixck8GUindBiKHVmdDi"}, + {"path": "m/0h/1", "xpub": "xpub6BdAYKv5PhVyQ6ebCY39SqWmQnjUPbAmc5ZEV4uZS7B3YdaagiYqUbpNx9MPn6qufzVdxFoitbx2qsBLr65cyvZNqD2qFNTQZZzA7WLcYou"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CPMXpWRcsZ87XtSBDMPQGn5NKagAqMPiqMgMnGYdSGcyPHVRkx2kzXoSUBt7nBQwVYDQP6R1ApnNC3kEbh2DM4iAuukgr4cNMtDtEsVZEz"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6Dm5SDi63dVvd7R2toTYyQJcCvfV8xLs9ckH3n6cvikaJD24z8gNgUH4ib8p8g368t5tXEqvLXA6nwnGFihf3EtSsj17uQyh3yXbiEFSdmo"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6FnV2hN7fEsADHMAber5YH97dEhwRogkWHN3mkt11XmBUQ2ymz96zpKm8pnwv9y4G7NRn5VhNL5XceBhrbPrjBaRV1Du9wP9w3ESzeFroT7"}, + {"path": "m/0", "xpub": "xpub68c3ytK59jdC5ixRhs6pVgQdQBMcVeRhRA9AXUKbekbJveQ7TaG3DmLU18Z1Ehdvsx4Q38GLARAqPXNf3JG61Bi8vVtTeSnc3RhX8WnxSph"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AzG2bJfPEHPAdMwrrwBj7cpxVCZV3L2PjY819qtxVxny9pmog8BxsDn4z7FUtiNBH8RgZSHVn2FpoyQNC9LtHxDJwCR3BUDdGtF3Sqsafj"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CdcHiixMpMhXipXG9C3YSEhvDmGiptR7694PQA8Gv2VB5pUPdGWgiTxnakXsjqjhS2yoRPcJuBUzpuYZNo8vLTAiXQVKvZZWduHKCz7Cts"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EkFyDYX4Xjnshe2SnpahMNHPDjniD6g1rw7L1K8WphiCkNqZNvAtb89r2NL1cBvFRQakrWYRAxZHkpawGyvVWgVLKhvvNhZjGrbA68J18U"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GTmGRJM7pbm7pAC5RMvpsj3q5G5fg4dKaWeaAJMA7rTpQv9ayHNtHkAidVyE7Nav4Y6ox5TJ7EQXkvjQWH6cb3vWEDUY2W7iwBGiL8NAne"}], + "xprv": "xprv9s21ZrQH143K3Rq4g5KhyZyKBhg9sWbhvDQmYXpFnB5Lq4zNiQVDexEoNAVuMhHBuF6JwaQeFs2YxvudoYhcgPMTixq63g6ukUcgTbM9y83", + "xpub": "xpub661MyMwAqRbcFuuXn6riLhv3jjWeGyKZHSLNLvDsLWcKhsKXFwoUCkZHDS1xEW91qFG2eh5jkZTzEkQc4uJM28Cx1JyrD3A7SM1ZzqYD5rp"}, + {"master_xpub": "xpub6BuDPjYMa6VWu4d8ysHJCTqGARk85g4UkxTyTwU7PWzohoeWcdUeX7gCpwwUQ3EF68bWaHBivffzdchv74oPp3BN2eZqYGDQzSXYYnVMgks", + "mnemonic": "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "vectors": [{"path": "m/0h", "xpub": "xpub68uW5iyXtqyqpsyXwLRLC1nQfSV3WN6wXLdbCfMDnYHRvotSE3dC79d4a4SHcCG8HUbik2uCf1jGhK29mGQQc3wuJY4cR1g4vQ8BuxKxqdy"}, + {"path": "m/0h/1", "xpub": "xpub6Bc6zisUnKTvSqoYE788ovZiR8eTM1CxAHrXyV8n4vogzkcfqQaTQdvonmMfe5PgcZtBBmW47juwQCqZqLUHdwaxZEcFJdaUVL54PSd1zKY"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CXccXFhWKgKMRPmPvNCrynm6eQFoYM8kC712j2yydgxgA6DE8x83RrRuWAYkmkiJJDNWu683d2qQcSicrSBgUKc8VW5m4kqXMVMF1ZsRxt"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6FD94cTDGSZ87LRCmLBNHDagGFBUJKe7RaAoWvHxpxhhgjAm62uGuyEZrtFxSCkhao59DHGM3bVkTchzHCT3wwRX478WdrN6VvWM76fZMFy"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6G29zDYRbJ3QVVo719P8FkN22BMhPR5ouBtP6DfKHzTkiszVCpXj6nnHE4LnEMH657J8JgxCghjYhs75i48UsjDRWKGcPypfKAQPQy6PNua"}, + {"path": "m/0", "xpub": "xpub68uW5iyPZBSsednaJHbRHdpjQ3thcAmq9XJiQXiUx5CP1zwUautB8CJNm21AsyJSTbfEvTrWCooxF1PQ67zZJED43Y7pHwN136nznHx715o"}, + {"path": "m/0/2147483647h", "xpub": "xpub6BHpcjfZNXFFLTWPTjMvt7sJEy8J38JhuiY9u2R9YKxQ2ddoxemv8BVqrb4UBPw8prvtHxrSNSfJrwURduH3KfEdw1J12LABoA12UcB1vAU"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CoVEP4JNY1BLKV2gYRHo7BcFGhTz6zpguww6pN1L33NxUV9VJEgecX2Cbv7ssuGd9bAR4YRgAvFhJDFH9o16o2FMxpPWtGfKCSyiMnxgZT"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EGocS7sqNGqiTy2p6rg4mQ3cn2QV4eyJkGByaGrmxCo1it2M9B2w9PYGQmn8v2fhFgkQyQG5aYsRyrKuj9G8zzo48LzCaBzEJvQ1RNZoCu"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6HFByTLakDVbWGiLjnkYbWtigb9VZzBsjzuDdjxH5McRJBA79weEt8fkTRKPVbn89EW5YQtzZEP6Fc7CXvJ1zisU3BZYz8aFzMgSQ8dBQRn"}], + "xprv": "xprv9s21ZrQH143K3gwvGvpNybNnKgucYunYCveFYLo6VSZAXnddMzk4FGmGt9eT8YyQ841i7Vt6VSYotTtby5QuB2QJraw2ta1GSaaZFWGizET", + "xpub": "xpub661MyMwAqRbcGB2PNxMPLjKWsik6xNWPa9ZrLjCi3n69QaxmuY4Jo55kjTu8X6tNUT1dra7VPBJ6XWgW1DeJ2Eh7d9vmcZeFAkZfWauMAtT"}, + {"master_xpub": "xpub6CQwAScCGPWPGNRrHJekRwCX73VTs6heupyRkmKgxzAwWcJW1AT3g3y6CrfwgVWLqmyxFR6QAhRoeZcr1XpNgi859LtzeWSLbkdGMN8Jpt4", + "mnemonic": "cat swing flag economy stadium alone churn speed unique patch report train", + "vectors": [{"path": "m/0h", "xpub": "xpub69WmLayNmhGX8F7ZmDJDwckniLVqGx9aKBYXwPEoXSSiuBRMc17Bgv1sfxNcVfu5CMiRSBRsxAqEFTyh761WSsStbhyiY5mkZtxrqFF4C6e"}, + {"path": "m/0h/1", "xpub": "xpub6Af6E9chBdS3XWnR7RSSoAVfjhRAgZMsbVkuv5kYfZSXMPnVtEN3whzeBpzfug6yfBNUx9f79T6FvT1Qix7HxDCyc1zwtggaTLWHUJVxuKF"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Cby5tczeFh4EogxVmZgyQcHaVWyQr4jpK7XbTb5njrGr64CNFNSnaMHMf7Z4k4LtqeiHKQEHo46EVYQpaVB4YwvY1vh7huCLuvWPn8G5pQ"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EHTdZ2Yc635arz2xSUn7HtdR9gh6xLS9hxPDbFFca2vMYgnhpqCxf1PvgCHmUZDKEW1DrFfSh1EhvdcGRWZSZRwEXK3ZaSwBrnt6y4cepT"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6HChqHNzpu6ciBbkgchLDqJibeu7yN5APjfo311izJBNZpsx7RgAWPtKh6fTocFmzebgEnuZUfffBeRPArqMWb7ww8KfpNV2ia76qAj3wba"}, + {"path": "m/0", "xpub": "xpub69WmLayES2jYy6srezQ2Lzqq937QPUMZH9oBKAEtRTYwhtbpv6aiANFWVGNYYVyMe6TcxUrLmz6L898FRpC2KKBG3dmKNt8SEuNsQ2CK6jw"}, + {"path": "m/0/2147483647h", "xpub": "xpub69yVugcVmf2oCKR2iMceXThBY95M43PNYyeWiP85GFtXtPwjwwLsVs5etAwKg2nQE5dqfLkuisK6zB9cWRX1VbZvnX7XYEoKYBwPb8VKMzR"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CvzXaQdm57yrxRsZto4RNmMQJvgiVx8TsTzZgfQz5w8JsSaawKXXCyVGvjannDc1bDqP2SkUF88Yy1aess5btmaxVuWj3EV59jpwB1B8zF"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6DuEQ3Thg3UPbg4fkevwAJ5wFi49YhkayumHXgHTAtqAWWZwzPRPJZAGR8jcvgD2fSmMFUQEwczP5ySCJAK7g6haG2kjFVS9nj1d6kwGLhj"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6H297CE2gjDy2doiDihd5qnvmog3LHv2tvo524nvoATjCJkZGPwWaCYpUtNpWwn1XQ2XWkMbDSrch7eDWnuU9LjiS8CTnzEaPjvmqdqtFXU"}], + "xprv": "xprv9s21ZrQH143K2AtEH5vXQTtJQdUVC1mwSKcGt3BMvKQDV5vgWNjAHYc9Kt7qw5CZCX2AXzprnVXurCCqPRiDkCamD4feECNCiYT4FNNmWBk", + "xpub": "xpub661MyMwAqRbcEexhP7TXmbq2xfJybUVnoYXsgRayUewCMtFq3v3QqLvdBBp19mJSGk7t735oQH622cd2ZkykhdESxxBwYoYTBS7wkwjuFTA"}, + {"master_xpub": "xpub6CrVe4dGQS48yjcEHT9hF2FtuegWpsmDnDThoRSpYvoH4es1bH9SoqFC3WPfYSFjpMA1K1VwDKNLK7edp4whfpJ3kDJKeHFMKcubtnnumX7", + "mnemonic": "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", + "vectors": [{"path": "m/0h", "xpub": "xpub69Qd43sFY9Uc1Kr1nXsyXbFU8ab1y9ysmD9cWcAeYLBemX88gUDw564fakABvfa4k1jvueQGx7wT3cmf9Ms6eFnZq1Mo3snGueQ65cuvbWH"}, + {"path": "m/0h/1", "xpub": "xpub6AjBfojrY6hQpqEbvnoDmqFsrrUbSPpQS27ty5TNDMEqoBZB6UqXnRwTcBWigduVMZ2Qd79Hx7TBSTWdNJWNbo63enFHZMDWewTKZbtqbUG"}, + {"path": "m/0h/1/2h", "xpub": "xpub6D21mwsKcVo5pVqN2jVwped7dbCjLd8nJK1fiLqQDw8zZMdHKPVXuwepJPUP7w97pRhD2TPYkyKMzj56uLJ4FWtfbBh34Px4NM3sedi31sn"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6DzQ2poTbeh8r7eu8ubD7XjmrmttLnr8Jnjas9WMUMTBonfQpLZqwoxHyTofPz28D4UwXE5BbwUSKqWRcnWHCwcUxy5hkdeFeLtJ4HobPLs"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6GYLXbKqZubF3gbrU1pZ9GL2GbUkVDtPja9ycva8dyhMYVJ7zvcV5aXNtYGsSGLCW3UnFXx9SSEX9kARwL5Vcek8CGdX52vdAcWPQcD9kVi"}, + {"path": "m/0", "xpub": "xpub69Qd43s7CUwdrLE9vCg3SY9kSwCHs2Rd3TLKEhcoxD4kak8vdwbT4FSLbzsfZx2h81hFsg7cUYCFrvSDMBCKGtvsNaSSRmDVS1tih8H3MVd"}, + {"path": "m/0/2147483647h", "xpub": "xpub6Aun4LKeJsDwD3sKkEar57EC4oGfLj9Rc4VGYBBFkSvbpdCkCagb6fZcvpX26xz42uyfjggwo4DaNDtZd7AB1QpjvHjsizXYNcmQFQrU1Jv"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DCTck92JeFMBUtSyuvfdrtwToVQbQbYq4v9NhJEojaW96ywhgrjdp9SNwMCASbHdfKw9k4pZPWhRoh3vLbR747ZDDECAV7Wgtw4YgrmXo9"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6Dj88k2Kg3cC7wbsdkCQADgDqt5x1qqccDjd2xE7sseb5HaXk4Fq7kpWpcEimrFyeh43n45YpR6w3Gx33TeFfNQZZfa9KdmuYsAKTNhDzn6"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6Gjqq27mfZBYuPejqaVrkEkDiYCYP9giPiGHFsZQ95tmrCm9sqBgGCUiwerm7MesUEwnouh5sMyzBMYsT32LL8r84BP1ZKYKEsrDWMBb8f3"}], + "xprv": "xprv9s21ZrQH143K38sXphyFNo98r6a7W6qQXzm7e2k1Xk8ttAcPxvFd8WDyM2WMqabevL3fHke8MrpKXDJS412sh3AzK3YGpxKeu2aLki4VT7c", + "xpub": "xpub661MyMwAqRbcFcwzvjWFjw5sQ8QbuZZFuDgiSR9d65fskxwYWTZsgJYTCKXDxZYhs6CYGJGAZu4UsdCdmtgT9oEZXjSL6hSF6JxqNGyFc4B"}, + {"master_xpub": "xpub6CjeCajsEoYVr8C5F7iQust2nZoA3q7cHWaSAKZqDuQtbmtMuZphhKHgsVGfjG9JXG5WJzB169vmLTMqdbddexrJ4DX8vkgG51Ax8WHQcPy", + "mnemonic": "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "vectors": [{"path": "m/0h", "xpub": "xpub68BjfutLj9SN8y2vKJkorGGxuWdzrVpnNUsKEmnCQmTdkc2MteUwoNsGWqhkUJzMDMkQW3Xie4HZBCbAcyK1ZP9d4UhhFXBZPMZeJpgJBa7"}, + {"path": "m/0h/1", "xpub": "xpub69swybbmnF39xshPftQ9nRacSEo75vRiawUJzWFjpXnEQyUJuotedTbgYt6gLcHZ669vopo6ZeDMecpf7ELXZtNnQ8QJWzYFvqwojCaxpQL"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Bj8p9GMXBoE1n5U5uFwEYx1ahX591Rixijnr1Sc32QDptfvGNDZiRix4P2Hw2yv4aJRyHuUp9pA1WLVBRckv4jHCSPbmSpwZaJSmTnUwAN"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6FQgi6EqZVmEfyZ8HaMAF91n23kYA6sV8hAn5Tgko4C8FMTJ3k7RjHqZLXUQfW35S7LB8p7rJqHu4bkx9p67bTPzaZrV8ETM7zjoaaTqyde"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6G5huJS7Gf3Y2KKYmWi2y8xCvK11a5XHnKBfkti3zcSbACZXZovd13BnSB3n8KFKKLgXCYQLFN9Y7gnFDFFEJXiTnFNYH5pt8MNGtoBz8ms"}, + {"path": "m/0", "xpub": "xpub68BjfutCPUuPxWwGcrvkJgXDTaK4y3xZpLwvXnxMAcqbK6e6LBVbZHYU4HCoK1XYtmXiJXdnsJKr6qRvRza9h8bUgk4A85VNZXjVqfD3yxs"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AkFTbmz5k3tKE9V2M3wttbVTAA1QC5r7uBWbL7FLVxXwaxgLbpRgGSkyhjzQrBZqWjoNg39wognbEQanEkDKX4SuzEBDpG6ptchrVVVpYn"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DAM9JVXUtEFudCcsq9UcvyyyaZdNrXYVnD8QQVo2ucan1zAjddbSuBoDnyT7YSy2Gr6WxV32sMrLz18n2rceCoHPo9zttCXUYbFa5cSpoo"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6FF3Podcbq7zmwHoWt9s8RsG69ZqjpRoBAUPRF4kJyR7P65Cd6twZNTWsqRqBsXaDX57JfjEKFsL2jRb6U29R4AqJX9iDcpXguHwLQGYxKc"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GazAxvg88uQUfLm3e2yDwA9C6hosFWoynUNVXMvcUMzpVjrLZ9xcQ7wtJDLjg9NLaZYdwuNytEeP4oTXe5qA6kHu9UsMUcdorKaiPswzSq"}], + "xprv": "xprv9s21ZrQH143K2Jkd9X2AWpbhNL6yLzVmDxr8F24GXpxAGnkFELJduv1KinexqDctfGBmf6gwrejTQ4QzYZGw6o9RRjpBveREQvytc4JSkPk", + "xpub": "xpub661MyMwAqRbcEnq6FYZAsxYRvMwTkTDcbBmj3QTt6AV99b5PmsctTiKoa4tf3rUQMR16kVYr3mDbnUsFaAuf1qXYPgiSP6ZvZ8wbiFmEVmJ"}, + {"master_xpub": "xpub6CzoMCg4goLhyKXiPWLqpUzGQbfSWvVnnDUC8TuDdSNeJhA6JkXRicACEnTg8rRiwLdkLH83S2jduHJgXnvjecJ4jS1iotZVMm2dwTnvTjY", + "mnemonic": "vessel ladder alter error federal sibling chat ability sun glass valve picture", + "vectors": [{"path": "m/0h", "xpub": "xpub69GiX9v5Cudsh69nhkYaFU2wKKWf7xreUVyxhS6DjnVqYBtbD27t4Cc3oEymbU24QAiVfVt7KGWZRXwhQBuFkV2Cuorzx3m8iW5eEisnp3Y"}, + {"path": "m/0h/1", "xpub": "xpub6BBsjHVgb7pMrC9SqtT229bJwc4dboeTboy2xsyBfKhJ4CM7CJx58XahxnrshiWNVFUMvMNZ6jfVRc5dwUDrAWU8dYvwTsb5tP3BaQSWaWy"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Crwinm3mHBrMcQKJ4SuVKdo7J4Prse6gvsBwMgFNKcpRBxayBJXF62U6u6sf2bchrwXcBPNP1T88cwstPxVxXp6H56iFraNbqZycgeyHzq"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6Ds1AYMa1JDQsZZSqDThuiXxV4ALscHbG1yuYUzqccpph1HxUwew42aSqPrWYL7t67WPngdvmwzV1FW35TdQs33B7AX1a5RGyn4w3kKQnuk"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6Fukgg2TFvaTV56ahWppUfitXytLXcgPSVUXtpUBH1aCfnM4nGBJe5xrpvR2m8cxU4gR8YeiTVawY6NNzMCGhvbXcUjctFee7cVP75oADEK"}, + {"path": "m/0", "xpub": "xpub69GiX9uvsF6uWzE7YPVCzo6SikJyPgyf3ZAxWiRVusDh8QUvYcCxQwEkBkpbftdTBmbJzhHVPDsys4mjPLPGLh9vnHVvrRL6xybY1r3qqSv"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AgEaEngfgfe1zMx2gamRiU1aymh8Q8iQ6W3civ23WUKWQijs7Ubd1YrxzkdtJ9Y9KoWwboGH1qNyzJFNsHZx1zujgGzb37mJ9TuV3LCDri"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6D4LGGCFHCYiGuoKrWzyvsf62uo6CibBYZUqNMEsNTSZ2ka7SQv9UP2BREuxG93LpDQNn2CxBzDJzksAgAagBZJR1eUBPfmFXaQZGuT8HL2"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6ExhbmAYyQkkZ7tiZux1XxpxPFar8SWnZEdHuWoLX27mdYLXTRd685sAENV1UeDpeTVHGFJa9Jx4tEYKyPGGfyXqZTqeDyHi47rSsRMonVN"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6G27GctfQAGGAAKQjbWTnUgBzdrKB4i7jCbH1chcRAucSYS1gaXP8EGS76fKoKdKmpNvb93cpKBS7ABp7aXYr53i2YqtmTogNiYMU5ndCVg"}], + "xprv": "xprv9s21ZrQH143K4HQ2EdzttnLzhVt7gkEHcrBWw8WZBiCuXzp37KcocvU9uKazS2qSqSVDp1e9d5F1PMyMs5dTGxSamJWXeMfEmyKGxtMGzKU", + "xpub": "xpub661MyMwAqRbcGmUVLfXuFvHjFXic6Cx8z577jWvAk3jtQo9Berw4AindkcW3Nqf5xVQ4G9ksujKjRmNdkz5YcvKyPAqqRuRyM1749LQpEdQ"}, + {"master_xpub": "xpub6CARLEj2PqtS8oMnvJ2vaFzRc3j8PJvuAZcXW11HJssQGxnGNFnQtPiwLvYFzqhSa8vyUNgURT17idGv8Co4qSbDzsxkv8accDZCiWit2Rd", + "mnemonic": "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", + "vectors": [{"path": "m/0h", "xpub": "xpub68CGNoi8Pn2PPHEBfjLrBcgQjrx1tm3vV1HDub8ougSACNrWCuLQYdVfcAViSVrikzVKVfHu4oqG2oWcpmi3hYVw2MJCtTjKmKpzuKBmC8J"}, + {"path": "m/0h/1", "xpub": "xpub6BLjroUgrqRhzkMszxub2x6ZmvvCrbmSgkoQkrAMn5SbjmhbEJKxsnpSLXDNJz4UPTMot9pKHD9Dp7zgG72ZzToQasbF9HkqJZjsLHdhbBj"}, + {"path": "m/0h/1/2h", "xpub": "xpub6CWVUWLrdisahEBPJHcUzxGwJL5ca1miwZDPgyjPfspxCJAmKvqojYgFYmVmUBJxz3nFVeQsq3T58wjC6ae4adhr2DdBwGSdy2qarKgX8Yc"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EF24pP2b5aTpHwmHit21ozZ5NYaaa92CmZg44aajekmxvcS2ccMkSUu54kU1Wp2u4Umq2QMgWFRTtqJsfwe39TnVJDjHzVT3F9VqtdY7xz"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6H1ZX1HRWeZ8i8QunX9ziNF2ehdaaA9CpqjZPk53b4WUVzd4zGSnRZPkofYcfHaDb4PnnMeUNXpKFzSP29ZpXkSuuXRqRxNJkvJKkGiZR9b"}, + {"path": "m/0", "xpub": "xpub68CGNohz47VRBnTrxFPnSn8fvKtCAbhR7R42R7bfBwP3sLx5rGEYhjyECfauXpuNnx4FFcratrmwo7XAC9fzKxRhFs9oULH3XMQXfKoSy42"}, + {"path": "m/0/2147483647h", "xpub": "xpub6A5ru4XhfrfsFBFHA5TbPvsNUTHtggySZTXCW6qR7jbLwXBhwJsCo9NxbCyEBnVnDSbL3PHyAAMv9vHUDY2q8dHpwAGW46uybHwHNBMmjHq"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6DRrDcxV7isD1F6hU1XWkdazniJXaxZZdzprN4WJS3romDiEdwhFPk1jt4i9sT7S6jJv2gzPkxgweNejUSgWtHfd1Dcx3YFrNysPMV5NSS7"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6DwRebtJfjEzbG5nPeANfmn5H9PBok1bnsJwTZEARFyZyhd95zgt3MNYdm43dz3J6Zv6EvZNBFct93vYip7VkRuwR5hvgGAjnA3DrBYuHyP"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6Fxs9sPKtAd3gU4yJ9TVz959FArAi7gGDj85eRQCueiTPMnwtfroMNuAjkXLLfy6bN2JQP5ZeZPNKBFP7acUwKEWCGZaJ3NFjF33Mi4Brd4"}], + "xprv": "xprv9s21ZrQH143K4ZQvNzA7Loca2W4jZJEp4y17EDDLY59hYBrK51KrGW3sPtC1BHKgyfqjR3JoY8cX3VMtHZAcjhzE1HB13zmyR2Vs7xXaq2T", + "xpub": "xpub661MyMwAqRbcH3VPV1h7hwZJaXuDxkxfSBvi2bcx6QggQzBTcYe6pJNMFAr4NJt68cwGraTCtbWbvcY7WAc2DaXUqSHNhDxUg4Mesv1XnVy"}, + {"master_xpub": "xpub6C32v2sJCVcgyZHU5kSBnnRokjcC66pmTeCjXJLxiLUWdD7NZmz4R8EmpCP24opNWFb8W2eZwbA235pdT2WEgQJqnQtY5K9JjQh5BJf6tkK", + "mnemonic": "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "vectors": [{"path": "m/0h", "xpub": "xpub69h9z6Y8DYUPVkNXbZs9ATK1ZLuicA7gitrZPExJdCDA9u2MJfkQridgXniA9wU74EjEQgzDnfF9e11Uwe5EcKnCwbQKGk7eit594cQ8dKB"}, + {"path": "m/0h/1", "xpub": "xpub69uiXN5JDEpHQ8VDFZM57Cse9pUo7R4oZscUEuVw4XvM7ocbhemcUZFXFNeeB53Mtn34wcg2thRw46pGR6CjQ4eiqb1qZHwVs5a7wUuqbxw"}, + {"path": "m/0h/1/2h", "xpub": "xpub6Cn3HJHDbZAakPpNoHADNF2ytYekNkRPGsDTkCY75SrKaQwR2Dgjh2rzTz3ASnGDJMCT5NY1m6vAcV4WTWW5oqGBUjwGvz8Eb6Z8wT9Smtu"}, + {"path": "m/0h/1/2h/2", "xpub": "xpub6EfqgCMa7eCDC9Sm4yMnRxWRwJiG8DPfpCpHUt9EnPGUbcpfzGNAG2sbJDSJYd1Er84A9VB1MDTfUn1CLcXbyqLD17gfYGokRqeNjsUjuKH"}, + {"path": "m/0h/1/2h/2/1000000000", "xpub": "xpub6H75RNPgHAcKkbyskTtYz91qZCxrrh1KjwBfUBC3oUgm9KpZTaEMoUBwPZs92UPCmgfN33HnZwWhfQ26HUSVqiph7sPuyY3zyoFDsrkVnUo"}, + {"path": "m/0", "xpub": "xpub69h9z6XysswRK7xm3NKdhqsgJSD4z5uwoh9UtRocymL7eH6PU3EJ13oGLSbnBLCz5KfpguHmsdNNGrHKBz11GnDpFho8zDaBDMZRd2qpy83"}, + {"path": "m/0/2147483647h", "xpub": "xpub6AkxvSSVjiMixBodUarR7W9Qgs9szQJLahiR3yuwoMh4pbkFEcrMXBrjZtLVAWn6u1wxAHMPdMT5U9vgHpQHyFovBNSGxQxh5aUfEPqRSTa"}, + {"path": "m/0/2147483647h/1", "xpub": "xpub6CTbKFLR2G63VAeTcC7kUGv9s2crQuoPL2EoxLCeHqzjbDZZSSBTJ5nGHGHV1Jq6TD51GibFRRRrDiq4MBKbPPH9iJFZ6qHv3w9nfT4f2By"}, + {"path": "m/0/2147483647h/1/2147483646h", "xpub": "xpub6EXmH5Tm3jv7DbX2hZXem45ZMixddQzQokmvDpqEkVxByidzCWbZHik6FwuptgsBxXrNgDgrwucay1BmunYA8dStWRB3M18tjwUgc5Hdyo8"}, + {"path": "m/0/2147483647h/1/2147483646h/2", "xpub": "xpub6GBKNafDD3DNusJEJbwZt8xUThnP6gwagtaMUkpsUEzx2HTZL2mPUrsr9Cfs6n3PBWZvHNGieTMrx8iJVkei9114H1cRQsdUXAYbXazEJ7U"}], + "xprv": "xprv9s21ZrQH143K3vkVeVcLG5PeVoexN6hpu9r4mS2j3uVeZo7vBrRNGHENDZXwYBgbQ5eMvHCX9YRL8V7aykC7a4UNkvJCuBacLRHwsdMGhNF", + "xpub": "xpub661MyMwAqRbcGQpxkX9LdDLP3qVSmZRgGNmfZpSLcF2dSbT4jPjcp5Yr4pYCzdVYPKEjCUdyU1oAQaUJhdaHUi6QyxXAL23cEpSBxXXDZtr"}] \ No newline at end of file diff --git a/test/test_trezor.py b/test/test_trezor.py index 4a50eee0d..c5c2bb6e1 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -13,7 +13,7 @@ from hwilib.devices.trezorlib.transport import enumerate_devices from hwilib.devices.trezorlib.transport.udp import UdpTransport -from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic, load_device_by_xprv +from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic from hwilib.devices.trezorlib import device, messages from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx @@ -132,7 +132,7 @@ def test_getxpub(self): with self.subTest(vector=vec): # Setup with xprv device.wipe(self.client) - load_device_by_xprv(client=self.client, xprv=vec['xprv'], pin='', passphrase_protection=False, label='test', language='english') + load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') # Test getmasterxpub gmxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub']) From 0f1c5b40174d815a4879f20bb1fd6637a8ef61ce Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 13 Nov 2019 14:15:03 -0500 Subject: [PATCH 010/634] Enable getxpub tests for trezor t --- test/test_trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_trezor.py b/test/test_trezor.py index c5c2bb6e1..164d20990 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -319,9 +319,9 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface)) if not model_t: suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_1_simulator', full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface)) suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface)) else: suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_t_simulator', full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) From 63acd0f2c41acb57d5c15763f37da89ddc17e584 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 5 Dec 2019 01:52:00 -0500 Subject: [PATCH 011/634] trezor tests: use a different invalid pin --- test/test_trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_trezor.py b/test/test_trezor.py index 164d20990..8648a15fa 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -210,7 +210,7 @@ def test_pins(self): self.assertEqual(result['error'], 'Non-numeric PIN provided') self.assertEqual(result['code'], -7) - result = self.do_command(self.dev_args + ['sendpin', '00000']) + result = self.do_command(self.dev_args + ['sendpin', '1111']) self.assertFalse(result['success']) # Make sure we get a needs pin message From b1fc5c74fddafd80f871a89217ffd11400160425 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 6 Dec 2019 19:03:02 -0500 Subject: [PATCH 012/634] Remove Bitcoin ppa and just use incompatible BDB We are already configuring bitcoind with --with-incompatible-bdb so there's no need to need the bitcoin ppa. --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e417794e5..d88087b4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,9 @@ cache: - test/work addons: apt: - sources: - - sourceline: 'ppa:bitcoin/bitcoin' packages: - - libdb4.8-dev - - libdb4.8++-dev + - libdb-dev + - libdb++-dev - build-essential - curl - git From b7a6d440eadf598425228a1f329aaa2137a36a73 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 13 Nov 2019 20:12:11 -0500 Subject: [PATCH 013/634] Build ledger emulator in travis --- .travis.yml | 5 ++- test/data/speculos-screen-text.patch | 55 ++++++++++++++++++++++++++++ test/setup_environment.sh | 32 ++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/data/speculos-screen-text.patch diff --git a/.travis.yml b/.travis.yml index d88087b4a..8f6a818b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,8 +37,11 @@ addons: - protobuf-compiler - cython3 - ccache + - qemu-user-static + - gcc-arm-linux-gnueabihf + - libc6-dev-armhf-cross install: - - pip install pipenv pysdl2 protobuf poetry + - pip install pipenv pysdl2 protobuf poetry pyqt5 construct mnemonic pyelftools # 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 diff --git a/test/data/speculos-screen-text.patch b/test/data/speculos-screen-text.patch new file mode 100644 index 000000000..925f8d9b5 --- /dev/null +++ b/test/data/speculos-screen-text.patch @@ -0,0 +1,55 @@ +From 3d4ff5c0f7ffa434f73990f03b21c20153d810da Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Fri, 15 Nov 2019 18:20:25 -0500 +Subject: [PATCH] Send out screen text over unix socket + +--- + mcu/bagl.py | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/mcu/bagl.py b/mcu/bagl.py +index e68ab9c..8c8b3f7 100644 +--- a/mcu/bagl.py ++++ b/mcu/bagl.py +@@ -1,4 +1,8 @@ + import binascii ++import json ++import os ++import socket ++ + from collections import namedtuple + from construct import * + +@@ -61,6 +65,8 @@ SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START = 0x00 + + DrawState = namedtuple('DrawState', 'x y width height colors bpp xx yy') + ++SCREEN_TEXT_SOCKET = '/tmp/ledger-screen.sock' ++ + class Bagl: + def __init__(self, m, width, height): + self.m = m +@@ -69,6 +75,8 @@ class Bagl: + + self.draw_state = DrawState(0, 0, 0, 0, [], 0, 0, 0) + ++ self.screen_text_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) ++ + def refresh(self): + self.m.update() + +@@ -566,6 +574,11 @@ class Bagl: + elif type_ == BAGL_LABEL: + self._display_bagl_labeline(component, context, halignment, valignment, baseline, char_height, strwidth, type_) + elif type_ == BAGL_LABELINE: ++ try: ++ data = {"y": component.y, "text": context.decode()} ++ self.screen_text_sock.sendto(json.dumps(data).encode(), SCREEN_TEXT_SOCKET) ++ except: ++ pass + self._display_bagl_labeline(component, context, halignment, valignment, baseline, char_height, strwidth, type_) + elif type_ == BAGL_ICON: + self._display_bagl_icon(component, context) +-- +2.24.0 + diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 0d7967629..4e8a00373 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -162,6 +162,38 @@ pipenv run make -j$(nproc) kkemu find . -name "emulator.img" -exec rm {} \; cd .. +# Clone ledger simulator Speculos if it doesn't exist, or update it if it does +speculos_setup_needed=false +if [ ! -d "speculos" ]; then + git clone --recursive https://github.com/LedgerHQ/speculos.git + cd speculos + speculos_setup_needed=true +else + cd speculos + 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 + speculos_setup_needed=true + fi +fi +# Apply patch to get screen info +git am ../../data/speculos-screen-text.patch + +# Build the simulator. This is cached, but it is also fast +mkdir -p build +cmake -Bbuild -H. +make -C build/ emu launcher +cd .. + # Clone bitcoind if it doesn't exist, or update it if it does bitcoind_setup_needed=false if [ ! -d "bitcoin" ]; then From 98ad46930041a9e8f73c1329b138c0736536b3b0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 13 Nov 2019 20:49:47 -0500 Subject: [PATCH 014/634] Add DongleServer from btchip-python --- hwilib/devices/btchip/btchipComm.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/hwilib/devices/btchip/btchipComm.py b/hwilib/devices/btchip/btchipComm.py index bc878b8c2..c67ada599 100644 --- a/hwilib/devices/btchip/btchipComm.py +++ b/hwilib/devices/btchip/btchipComm.py @@ -145,3 +145,35 @@ def close(self): except: pass self.opened = False + +class DongleServer(Dongle): + + def __init__(self, server, port, debug=False): + self.server = server + self.port = port + self.debug = debug + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.socket.connect((self.server, self.port)) + except: + raise BTChipException("Proxy connection failed") + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + self.socket.send(struct.pack(">I", len(apdu))) + self.socket.send(apdu) + size = struct.unpack(">I", self.socket.recv(4))[0] + response = self.socket.recv(size) + sw = struct.unpack(">H", self.socket.recv(2))[0] + if self.debug: + print("<= %s%.2x" % (hexlify(response), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return bytearray(response) + + def close(self): + try: + self.socket.close() + except: + pass From 14fe8f0f4c658470fddda6e1a3038c7e270f21ea Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 13 Nov 2019 21:31:41 -0500 Subject: [PATCH 015/634] Be able to connect to an enumerate ledger emulator --- hwilib/devices/ledger.py | 45 +++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 26edcddf7..4fc24c25f 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -4,7 +4,7 @@ from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError, common_err_msgs, handle_errors from .btchip.bitcoinTransaction import bitcoinTransaction from .btchip.btchip import btchip -from .btchip.btchipComm import HIDDongleHIDAPI +from .btchip.btchipComm import DongleServer, HIDDongleHIDAPI from .btchip.btchipException import BTChipException from .btchip.btchipUtils import compress_public_key import base64 @@ -16,6 +16,8 @@ import logging import re +SIMULATOR_PATH = 'tcp:127.0.0.1:9999' + LEDGER_VENDOR_ID = 0x2c97 LEDGER_DEVICE_IDS = [ 0x0001, # Ledger Nano S @@ -72,11 +74,19 @@ class LedgerClient(HardwareWalletClient): def __init__(self, path, password=''): super(LedgerClient, self).__init__(path, password) - device = hid.device() - device.open_path(path.encode()) - device.set_nonblocking(True) - self.dongle = HIDDongleHIDAPI(device, True, logging.getLogger().getEffectiveLevel() == logging.DEBUG) + if path.startswith('tcp'): + split_path = path.split(':') + server = split_path[1] + port = int(split_path[2]) + self.dongle = DongleServer(server, port, logging.getLogger().getEffectiveLevel() == logging.DEBUG) + else: + device = hid.device() + device.open_path(path.encode()) + device.set_nonblocking(True) + + self.dongle = HIDDongleHIDAPI(device, True, logging.getLogger().getEffectiveLevel() == logging.DEBUG) + self.app = btchip(self.dongle) # Must return a dict with the xpub @@ -362,4 +372,29 @@ def enumerate(password=''): client.close() results.append(d_data) + + # Check if the simulator is there + client = None + try: + client = LedgerClient(SIMULATOR_PATH, password) + + d_data = {} + d_data['type'] = 'ledger' + d_data['model'] = 'ledger_nano_s_simulator' + d_data['path'] = SIMULATOR_PATH + d_data['needs_pin_sent'] = False + d_data['needs_passphrase_sent'] = False + + master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] + d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub) + d_data['needs_pin_sent'] = False + d_data['needs_passphrase_sent'] = False + + results.append(d_data) + except BTChipException: + pass + + if client: + client.close() + return results From 56eb7857acaac43112e0c287036a39395d457251 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 14 Nov 2019 15:04:11 -0500 Subject: [PATCH 016/634] tests, ledger: use ledger simulator --- .travis.yml | 2 +- test/run_tests.py | 20 +++-- test/test_ledger.py | 177 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 162 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f6a818b9..ed56c07d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python os: linux -dist: xenial +dist: bionic python: - '3.6.8' cache: diff --git a/test/run_tests.py b/test/run_tests.py index 87cccca52..3dee57400 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -29,11 +29,9 @@ coldcard_group.add_argument('--no-coldcard', dest='coldcard', help='Do not run Coldcard test with simulator', action='store_false') coldcard_group.add_argument('--coldcard', dest='coldcard', help='Run Coldcard test with simulator', action='store_true') -ledger_s_group = parser.add_mutually_exclusive_group() -ledger_s_group.add_argument('--ledger-s', help='Run physical Ledger Nano S tests.', action='store_true') - -ledger_x_group = parser.add_mutually_exclusive_group() -ledger_x_group.add_argument('--ledger-x', help='Run physical Ledger Nano X tests.', action='store_true') +ledger_group = parser.add_mutually_exclusive_group() +ledger_group.add_argument('--no-ledger', dest='ledger', help='Do not run Ledger test with emulator', action='store_false') +ledger_group.add_argument('--ledger', dest='ledger', help='Run Ledger test with emulator', action='store_true') keepkey_group = parser.add_mutually_exclusive_group() keepkey_group.add_argument('--no-keepkey', dest='keepkey', help='Do not run Keepkey test with emulator', action='store_false') @@ -48,12 +46,13 @@ parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') parser.add_argument('--bitbox-path', dest='bitbox_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') +parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py') parser.add_argument('--all', help='Run tests on all existing simulators', default=False, action='store_true') parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library') -parser.set_defaults(trezor=False, trezor_t=False, coldcard=False, keepkey=False, bitbox=False) +parser.set_defaults(trezor=False, trezor_t=False, coldcard=False, keepkey=False, bitbox=False, ledger=False) args = parser.parse_args() # Run tests @@ -71,8 +70,9 @@ args.coldcard = True args.keepkey = True args.bitbox = True + args.ledger = True -if args.trezor or args.trezor_t or args.coldcard or args.ledger_s or args.ledger_x or args.keepkey or args.bitbox: +if args.trezor or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox: # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) @@ -86,10 +86,8 @@ suite.addTest(trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True)) if args.keepkey: suite.addTest(keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface)) - if args.ledger_s: - suite.addTest(ledger_test_suite("ledger_nano_s", rpc, userpass, args.interface)) - if args.ledger_x: - suite.addTest(ledger_test_suite("ledger_nano_x", rpc, userpass, args.interface)) + if args.ledger: + suite.addTest(ledger_test_suite(args.ledger_path, rpc, userpass, args.interface)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) sys.exit(not result.wasSuccessful()) diff --git a/test/test_ledger.py b/test/test_ledger.py index 3f8e3b638..d441c6a3c 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -1,28 +1,150 @@ #! /usr/bin/env python3 import argparse +import json +import os +import subprocess +import signal +import socket +import time import unittest -from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx +from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx +from threading import Thread from hwilib.cli import process_commands -def ledger_test_suite(device_model, rpc, userpass, interface): - # Look for real ledger using HWI API(self-referential, but no other way) - enum_res = process_commands(['enumerate']) - path = None - master_xpub = None - fingerprint = None - for device in enum_res: - if device['type'] == 'ledger': - fingerprint = device['fingerprint'] - path = device['path'] - master_xpub = process_commands(['-f', fingerprint, 'getmasterxpub'])['xpub'] - break - assert(path is not None and master_xpub is not None and fingerprint is not None) +SCREEN_TEXT_SOCKET = '/tmp/ledger-screen.sock' +KEYBOARD_PORT = 1235 + + +class ScreenTextThread(Thread): + def get_screen_text(self, use_timeout=False): + if use_timeout: + self.sock.settimeout(5) + else: + self.sock.settimeout(None) + data_str = self.sock.recv(200) + if len(data_str) == 0: + return '' + data = json.loads(data_str.decode()) + + text = '' + if data['y'] == 12: # Upper line + text = data['text'] + text += self.get_screen_text() # Get next line + elif data['y'] == 26 or data['y'] == 28: # lower line or single line + text = data['text'] + return text + + def run(self): + self.running = True + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + while True: + try: + self.sock.bind(SCREEN_TEXT_SOCKET) + break + except: + os.remove(SCREEN_TEXT_SOCKET) + + self.key_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.key_sock.connect(('127.0.0.1', KEYBOARD_PORT)) + + seen_msg_hash = False + while True: + try: + text = self.get_screen_text(False) + break + except: + continue + while self.running: + just_wait = False + if text.startswith('Address') or text.startswith('Message hash') or text.startswith("Reviewoutput") or text.startswith("Amount") or text.startswith("Fees") or text == 'Confirmtransaction': + time.sleep(0.05) + self.key_sock.send(b'Rr') + if text.startswith('Message hash'): + seen_msg_hash = True + elif text == 'Approve' or text.startswith('Accept'): + time.sleep(0.05) + self.key_sock.send(b'LRlr') + elif text == 'Signmessage': + time.sleep(0.05) + if seen_msg_hash: + self.key_sock.send(b'LRlr') + seen_msg_hash = False + else: + self.key_sock.send(b'Rr') + elif text == 'Cancel' or text == 'Reject': + time.sleep(0.05) + self.key_sock.send(b'Ll') + else: + # For everything else, do nothing and wait for next text + just_wait = True + + try: + if just_wait: + # Main screen, don't do anything + new_text = self.get_screen_text(False) + else: + # Try to fetch the next text + # If it times out, maybe our input didn't make it, so try processing text again + new_text = self.get_screen_text(True) + text = new_text + except: + continue + + self.sock.close() + + def stop(self): + self.running = False + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + self.key_sock.close() + os.remove(SCREEN_TEXT_SOCKET) + +class LedgerEmulator(DeviceEmulator): + def __init__(self, path): + self.emulator_path = path + self.emulator_proc = None + + def start(self): + # Start the emulator + self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '-bn', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) + # Wait for simulator to be up + while True: + try: + enum_res = process_commands(['enumerate']) + found = False + for dev in enum_res: + if dev['type'] == 'ledger' and 'error' not in dev: + found = True + break + if found: + break + except Exception as e: + print(str(e)) + pass + time.sleep(0.5) + + self.kp_thread = ScreenTextThread() + self.kp_thread.start() + + def stop(self): + self.kp_thread.stop() + os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) + os.waitpid(self.emulator_proc.pid, 0) + self.kp_thread.join() + +def ledger_test_suite(emulator, rpc, userpass, interface): # Ledger specific disabled command tests class TestLedgerDisabledCommands(DeviceTestCase): + def setUp(self): + self.emulator.start() + + def tearDown(self): + self.emulator.stop() + def test_pin(self): result = self.do_command(self.dev_args + ['promptpin']) self.assertIn('error', result) @@ -64,22 +186,27 @@ def test_backup(self): self.assertEqual(result['error'], 'The Ledger Nano S and X do not support creating a backup via software') self.assertEqual(result['code'], -9) + device_model = 'ledger_nano_s_simulator' + path = 'tcp:127.0.0.1:9999' + master_xpub = 'xpub6Cak8u8nU1evR4eMoz5UX12bU9Ws5RjEgq2Kq1RKZrsEQF6Cvecoyr19ZYRikWoJo16SXeft5fhkzbXcmuPfCzQKKB9RDPWT8XnUM62ieB9' + fingerprint = 'f5acc2fd' + dev_emulator = LedgerEmulator(emulator) + # Generic Device tests suite = unittest.TestSuite() - suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) return suite if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Test Ledger implementation on physical device') + parser = argparse.ArgumentParser(description='Test Ledger implementation') + parser.add_argument('emulator', help='Path to the ledger emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') - parser.add_argument('device_model', help='Device model', choices=['ledger_nano_s', 'ledger_nano_x']) parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') args = parser.parse_args() @@ -87,5 +214,5 @@ def test_backup(self): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = ledger_test_suite(args.device_model, rpc, userpass, args.interface) + suite = ledger_test_suite(args.emulator, rpc, userpass, args.interface) unittest.TextTestRunner(verbosity=2).run(suite) From 14c656fcc3e3a0b43d3cc138c16f21d8f5410719 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 18 Nov 2019 21:30:40 -0500 Subject: [PATCH 017/634] tests: Allow device arguments to override --all --- test/run_tests.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index 3dee57400..9aeb17c4b 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -52,7 +52,7 @@ parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library') -parser.set_defaults(trezor=False, trezor_t=False, coldcard=False, keepkey=False, bitbox=False, ledger=False) +parser.set_defaults(trezor=None, trezor_t=None, coldcard=None, keepkey=None, bitbox=None, ledger=None) args = parser.parse_args() # Run tests @@ -65,12 +65,21 @@ suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) if args.all: - args.trezor = True - args.trezor_t = True - args.coldcard = True - args.keepkey = True - args.bitbox = True - args.ledger = True + # Default all true unless overridden + args.trezor = True if args.trezor is None else args.trezor + args.trezor_t = True if args.trezor_t is None else args.trezor_t + args.coldcard = True if args.coldcard is None else args.coldcard + args.keepkey = True if args.keepkey is None else args.keepkey + args.bitbox = True if args.bitbox is None else args.bitbox + args.ledger = True if args.ledger is None else args.ledger +else: + # Default all false unless overridden + args.trezor = False if args.trezor is None else args.trezor + args.trezor_t = False if args.trezor_t is None else args.trezor_t + args.coldcard = False if args.coldcard is None else args.coldcard + args.keepkey = False if args.keepkey is None else args.keepkey + args.bitbox = False if args.bitbox is None else args.bitbox + args.ledger = False if args.ledger is None else args.ledger if args.trezor or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox: # Start bitcoind From c0f526188a085f00e9bace6cedc6f36fa81344ad Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 18 Nov 2019 17:55:01 -0500 Subject: [PATCH 018/634] Use travis-wait-enhanced to avoid travis timeouts [travis-wait-enhanced](https://github.com/crazy-max/travis-wait-enhanced) allows us to avoid travis timeouts due to no log output while still being able to see the output of our tests. --- .travis.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed56c07d5..01214e61d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,6 +40,11 @@ addons: - qemu-user-static - gcc-arm-linux-gnueabihf - libc6-dev-armhf-cross +before_install: + - | + wget -qO- "https://github.com/crazy-max/travis-wait-enhanced/releases/download/v1.0.0/travis-wait-enhanced_1.0.0_linux_x86_64.tar.gz" | tar -zxvf - travis-wait-enhanced + mv travis-wait-enhanced /home/travis/bin/ + travis-wait-enhanced --version install: - pip install pipenv pysdl2 protobuf poetry pyqt5 construct mnemonic pyelftools # From trezor-mcu to get the correct protobuf version @@ -67,13 +72,13 @@ jobs: script: cd test; poetry run ./run_tests.py - name: With process_commands interface stage: test - script: cd test; poetry run ./run_tests.py --all --interface=library + script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=library - name: With command line interface stage: test - script: cd test; poetry run ./run_tests.py --all --interface=cli + script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - name: With stdin interface stage: test - script: cd test; poetry run ./run_tests.py --all --interface=stdin + script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=stdin - name: With wheel command line interface stage: test services: docker @@ -83,7 +88,7 @@ jobs: - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_dist.sh" - sudo chown -R `whoami`:`whoami` dist/ - pip install dist/*.whl - - cd test; ./run_tests.py --all --interface=cli + - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - name: With sdist command line interface stage: test services: docker @@ -93,7 +98,7 @@ jobs: - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_dist.sh" - sudo chown -R `whoami`:`whoami` dist/ - pip install dist/*.tar.gz - - cd test; ./run_tests.py --all --interface=cli + - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - name: With linux binary distribution command line interface stage: test services: docker @@ -102,7 +107,7 @@ jobs: script: - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_wine.sh && contrib/build_dist.sh" - sudo chown -R `whoami`:`whoami` dist/ - - cd test; poetry run ./run_tests.py --all --interface=bindist + - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=bindist - cd ..; sha256sum dist/* - name: macOS binary distribution (no tests) stage: test @@ -112,6 +117,7 @@ jobs: addons: artifacts: working_dir: dist + before_install: install: - brew update && brew upgrade pyenv - brew install libusb From e085f99add575f429763875e1c1af2c28312e393 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 4 Dec 2019 19:31:50 -0500 Subject: [PATCH 019/634] ledger test: hide TestSignTx behind switch TestSignTx fails on travis, so put it behind a command line switch. We can run it locally if needed, but it will cause travis to fail. --- test/test_ledger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_ledger.py b/test/test_ledger.py index d441c6a3c..0bd51e361 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -135,7 +135,7 @@ def stop(self): os.waitpid(self.emulator_proc.pid, 0) self.kp_thread.join() -def ledger_test_suite(emulator, rpc, userpass, interface): +def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): # Ledger specific disabled command tests class TestLedgerDisabledCommands(DeviceTestCase): @@ -198,9 +198,10 @@ def test_backup(self): suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + if signtx: + suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) return suite if __name__ == '__main__': @@ -208,11 +209,12 @@ def test_backup(self): parser.add_argument('emulator', help='Path to the ledger emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') + parser.add_argument('--signtx', help='Run the transaction signing tests too', action='store_true') args = parser.parse_args() # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = ledger_test_suite(args.emulator, rpc, userpass, args.interface) + suite = ledger_test_suite(args.emulator, rpc, userpass, args.interface, args.signtx) unittest.TextTestRunner(verbosity=2).run(suite) From 08d54cc8bffa3a1d0ca2c191bf3712db9618ea37 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 11 Dec 2019 00:29:54 -0500 Subject: [PATCH 020/634] Disable sending the passphrase on Trezor T --- hwilib/devices/trezor.py | 2 ++ hwilib/devices/trezorlib/client.py | 2 +- hwilib/devices/trezorlib/debuglink.py | 8 +++++++- hwilib/devices/trezorlib/ui.py | 8 +++++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 14b2147ab..b013e4967 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -110,6 +110,8 @@ def __init__(self, path, password=''): def _check_unlocked(self): self.client.init_device() + if self.client.features.model == 'T': + self.client.ui.disallow_passphrase() if self.client.features.pin_protection and not self.client.features.pin_cached: raise DeviceNotReadyError('{} is locked. Unlock by using \'promptpin\' and then \'sendpin\'.'.format(self.type)) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index 75125b492..b64e1c13b 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -135,7 +135,7 @@ def _callback_passphrase(self, msg): else: try: passphrase = self.ui.get_passphrase() - except exceptions.Cancelled: + except: self.call_raw(messages.Cancel()) raise diff --git a/hwilib/devices/trezorlib/debuglink.py b/hwilib/devices/trezorlib/debuglink.py index 9a94bfbb1..b7c8a40b9 100644 --- a/hwilib/devices/trezorlib/debuglink.py +++ b/hwilib/devices/trezorlib/debuglink.py @@ -159,6 +159,7 @@ def __init__(self, debuglink: DebugLink): self.pin = None self.passphrase = "sphinx of black quartz, judge my wov" self.input_flow = None + self.return_passphrase = True def button_request(self, code): if self.input_flow is None: @@ -177,8 +178,13 @@ def get_pin(self, code=None): else: return self.debuglink.read_pin_encoded() + def disallow_passphrase(self): + self.return_passphrase = False + def get_passphrase(self): - return self.passphrase + if self.return_passphrase: + return self.passphrase + raise ValueError('Passphrase from Host is not allowed for Trezor T') class TrezorClientDebugLink(TrezorClient): diff --git a/hwilib/devices/trezorlib/ui.py b/hwilib/devices/trezorlib/ui.py index 23f83f7ab..dafcae146 100644 --- a/hwilib/devices/trezorlib/ui.py +++ b/hwilib/devices/trezorlib/ui.py @@ -63,6 +63,7 @@ def __init__(self, passphrase): self.pinmatrix_shown = False self.prompt_shown = False self.always_prompt = False + self.return_passphrase = True def button_request(self, code): if not self.prompt_shown: @@ -73,8 +74,13 @@ def button_request(self, code): def get_pin(self, code=None): raise NotImplementedError('get_pin is not needed') + def disallow_passphrase(self): + self.return_passphrase = False + def get_passphrase(self): - return self.passphrase + if self.return_passphrase: + return self.passphrase + raise ValueError('Passphrase from Host is not allowed for Trezor T') def mnemonic_words(expand=False, language="english"): if expand: From 35d994af5b09403644b6403bb39b68ac16a0d284 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 14:23:04 -0500 Subject: [PATCH 021/634] tests: Use sortedmulti descriptors Some devices require multisigs with sorted keys, so use sortedmulti --- test/test_device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index bfb408711..4a11a1b9c 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -361,9 +361,9 @@ def _test_signtx(self, input_type, multisig): pkh_info['desc'][4:-10]] # Get the descriptors with their checksums - sh_multi_desc = self.wrpc.getdescriptorinfo('sh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] - sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(multi(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] - wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(multi(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] + sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] + sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] + wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti"} sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti"} From 51eac02552bcde9891fdf7bb0083ca820a828d0c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 18:44:50 -0500 Subject: [PATCH 022/634] coldcard: do multiple passes when the same key is in an input multiple times --- hwilib/devices/coldcard.py | 106 ++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 55ea54558..f6cebd822 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -6,7 +6,8 @@ from .ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH -from ..base58 import xpub_main_2_test +from ..base58 import get_xpub_fingerprint, xpub_main_2_test +from ..serializations import PSBT from hashlib import sha256 import base64 @@ -68,53 +69,72 @@ def _get_fingerprint_hex(self): def sign_tx(self, tx): self.device.check_mitm() - # Get psbt in hex and then make binary - fd = io.BytesIO(base64.b64decode(tx.serialize())) - - # learn size (portable way) - sz = fd.seek(0, 2) - fd.seek(0) - - left = sz - chk = sha256() - for pos in range(0, sz, MAX_BLK_LEN): - here = fd.read(min(MAX_BLK_LEN, left)) - if not here: + # Get this devices master key fingerprint + xpub = self.device.send_recv(CCProtocolPacker.get_xpub('m/0\''), timeout=None) + master_fp = get_xpub_fingerprint(xpub) + + # For multisigs, we may need to do multiple passes if we appear in an input multiple times + passes = 1 + for psbt_in in tx.inputs: + our_keys = 0 + for key in psbt_in.hd_keypaths.keys(): + keypath = psbt_in.hd_keypaths[key] + if keypath[0] == master_fp and key not in psbt_in.partial_sigs: + our_keys += 1 + if our_keys > passes: + passes = our_keys + + for i in range(0, passes): + # Get psbt in hex and then make binary + fd = io.BytesIO(base64.b64decode(tx.serialize())) + + # learn size (portable way) + sz = fd.seek(0, 2) + fd.seek(0) + + left = sz + chk = sha256() + for pos in range(0, sz, MAX_BLK_LEN): + here = fd.read(min(MAX_BLK_LEN, left)) + if not here: + break + left -= len(here) + result = self.device.send_recv(CCProtocolPacker.upload(pos, sz, here)) + assert result == pos + chk.update(here) + + # do a verify + expect = chk.digest() + result = self.device.send_recv(CCProtocolPacker.sha256()) + assert len(result) == 32 + if result != expect: + raise DeviceFailureError("Wrong checksum:\nexpect: %s\n got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii'))) + + # start the signing process + ok = self.device.send_recv(CCProtocolPacker.sign_transaction(sz, expect), timeout=None) + assert ok is None + if self.device.is_simulator: + self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) + + print("Waiting for OK on the Coldcard...", file=sys.stderr) + + while 1: + time.sleep(0.250) + done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) + if done is None: + continue break - left -= len(here) - result = self.device.send_recv(CCProtocolPacker.upload(pos, sz, here)) - assert result == pos - chk.update(here) - - # do a verify - expect = chk.digest() - result = self.device.send_recv(CCProtocolPacker.sha256()) - assert len(result) == 32 - if result != expect: - raise DeviceFailureError("Wrong checksum:\nexpect: %s\n got: %s" % (b2a_hex(expect).decode('ascii'), b2a_hex(result).decode('ascii'))) - - # start the signing process - ok = self.device.send_recv(CCProtocolPacker.sign_transaction(sz, expect), timeout=None) - assert ok is None - if self.device.is_simulator: - self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) - - print("Waiting for OK on the Coldcard...", file=sys.stderr) - while 1: - time.sleep(0.250) - done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) - if done is None: - continue - break + if len(done) != 2: + raise DeviceFailureError('Failed: %r' % done) - if len(done) != 2: - raise DeviceFailureError('Failed: %r' % done) + result_len, result_sha = done - result_len, result_sha = done + result = self.device.download_file(result_len, result_sha, file_number=1) - result = self.device.download_file(result_len, result_sha, file_number=1) - return {'psbt': base64.b64encode(result).decode()} + tx = PSBT() + tx.deserialize(base64.b64encode(result).decode()) + return {'psbt': tx.serialize()} # Must return a base64 encoded string with the signed message # The message can be any string. keypath is the bip 32 derivation path for the key to sign with From c85c2175d670894c996d4d0019d1052a721c2139 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 21:14:29 -0500 Subject: [PATCH 023/634] tests, coldcard: cleanup backup files --- test/test_coldcard.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 0013e57f0..d6bd83a4c 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -62,6 +62,8 @@ def test_backup(self): result = self.do_command(self.dev_args + ['backup']) self.assertTrue(result['success']) self.assertIn('The backup has been written to', result['message']) + backup_filename = result['message'].split(' ')[-1] + os.remove(backup_filename) def test_pin(self): result = self.do_command(self.dev_args + ['promptpin']) From 88db32c319f8c21a16ac9aa3b3b0fe61587348c0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 17:59:07 -0500 Subject: [PATCH 024/634] tests, coldcard: Add multisig setup and tests for coldcard --- test/data/coldcard-multisig-setup.patch | 29 +++++++++++++++++++++++++ test/setup_environment.sh | 3 ++- test/test_coldcard.py | 2 +- test/test_device.py | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 test/data/coldcard-multisig-setup.patch diff --git a/test/data/coldcard-multisig-setup.patch b/test/data/coldcard-multisig-setup.patch new file mode 100644 index 000000000..9ada99967 --- /dev/null +++ b/test/data/coldcard-multisig-setup.patch @@ -0,0 +1,29 @@ +From 86e11ded21ffe839cb907e2096fd7bc832c79ce3 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Tue, 17 Dec 2019 17:56:05 -0500 +Subject: [PATCH] Change default simulator multisig + +--- + unix/frozen-modules/sim_settings.py | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py +index db78972..9565078 100644 +--- a/unix/frozen-modules/sim_settings.py ++++ b/unix/frozen-modules/sim_settings.py +@@ -62,7 +62,11 @@ if '-m' in sys.argv: + sim_defaults['multisig'] = [["CC-2-of-4", [2, 4], [[1130956047, "tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP"], [3503269483, "tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax"], [2389277556, "tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM"], [3190206587, "tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt"]], {"pp": "48'/1'/0'/1'", "ch": "XTN", "ft": 26}]] + else: + # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator +- sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] ++ sim_defaults['multisig'] = [ ++ ['mstest', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 8, 'ch': 'XTN'}], ++ ['mstest1', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 14, 'ch': 'XTN'}], ++ ['mstest2', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 26, 'ch': 'XTN'}], ++ ] + sim_defaults['fee_limit'] = -1 + + if '--xfp' in sys.argv: +-- +2.24.1 + diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 4e8a00373..1e889d82d 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -63,7 +63,7 @@ if [ ! -d "firmware" ]; then coldcard_setup_needed=true else cd firmware - git reset --hard HEAD^ # Undo git-am for checking and updating + git reset --hard HEAD~2 # Undo git-am for checking and updating git fetch # Determine if we need to pull. From https://stackoverflow.com/a/3278427 @@ -81,6 +81,7 @@ else fi # Apply patch to make simulator work in linux environments git am ../../data/coldcard-linux-sock.patch +git am ../../data/coldcard-multisig-setup.patch # Build the simulator. This is cached, but it is also fast cd unix diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 0013e57f0..96e367b07 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -13,7 +13,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface): # Start the Coldcard simulator - coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator)], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) + coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator), '-m'], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: diff --git a/test/test_device.py b/test/test_device.py index 4a11a1b9c..8ffc1870d 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -419,7 +419,7 @@ def _test_signtx(self, input_type, multisig): # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'} - supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey'} + supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'} if self.full_type not in supports_mixed: self._test_signtx("legacy", self.full_type in supports_multisig) self._test_signtx("segwit", self.full_type in supports_multisig) From eea579c9a07c9616607f7e1bf82c8ab408b19c5f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 20:46:27 -0500 Subject: [PATCH 025/634] trezor: better handle when we already have signatures for an input --- hwilib/devices/trezor.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 14b2147ab..924fe76d0 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -206,12 +206,16 @@ def ignore_input(): continue # Find key to sign with - found = False + found = False # Whether we have found a key to sign with + found_in_sigs = False # Whether we have found one of our keys in the signatures our_keys = 0 for key in psbt_in.hd_keypaths.keys(): keypath = psbt_in.hd_keypaths[key] - if keypath[0] == master_fp and key not in psbt_in.partial_sigs: - if not found: + if keypath[0] == master_fp: + if key in psbt_in.partial_sigs: # This key already has a signature + found_in_sigs = True + continue + if not found: # This key does not have a signature and we don't have a key to sign with yet txinputtype.address_n = keypath[1:] found = True our_keys += 1 @@ -220,10 +224,12 @@ def ignore_input(): if our_keys > passes: passes = our_keys - if not found: + if not found and not found_in_sigs: # None of our keys were in hd_keypaths or in partial_sigs # This input is not one of ours ignore_input() continue + elif not found and found_in_sigs: # All of our keys are in partial_sigs, ignore whatever signature is produced for this input + to_ignore.append(input_num) # append to inputs inputs.append(txinputtype) From b5c6ea080a07fbc5ca416897ade9a4b89612d594 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 20:47:01 -0500 Subject: [PATCH 026/634] tests: whitelist devices for testing external inputs --- test/test_device.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 8ffc1870d..eb97b80bc 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -335,7 +335,7 @@ def _generate_and_finalize(self, unknown_inputs, psbt): self.assertTrue(self.wrpc.testmempoolaccept([finalize_res['hex']])[0]["allowed"]) return finalize_res['hex'] - def _test_signtx(self, input_type, multisig): + def _test_signtx(self, input_type, multisig, external): # Import some keys to the watch only wallet and send coins to them keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '40']) import_result = self.wrpc.importmulti(keypool_desc) @@ -408,8 +408,9 @@ def _test_signtx(self, input_type, multisig): self.assertTrue((i + 1) * in_amt == self.wrpc.getbalance("*", 0, True)) psbt = self.wrpc.walletcreatefundedpsbt([], [{self.wpk_rpc.getnewaddress('', 'legacy'): (i + 1) * out_amt}, {self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): (i + 1) * out_amt}, {self.wpk_rpc.getnewaddress('', 'bech32'): (i + 1) * out_amt}], 0, {'includeWatching': True, 'subtractFeeFromOutputs': [0, 1, 2]}, True) - # Sign with unknown inputs in two steps - self._generate_and_finalize(True, psbt) + if external: + # Sign with unknown inputs in two steps + self._generate_and_finalize(True, psbt) # Sign all inputs all at once final_tx = self._generate_and_finalize(False, psbt) @@ -419,12 +420,13 @@ def _test_signtx(self, input_type, multisig): # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'} - supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'} + supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} + supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'} if self.full_type not in supports_mixed: - self._test_signtx("legacy", self.full_type in supports_multisig) - self._test_signtx("segwit", self.full_type in supports_multisig) + self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external) + self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external) else: - self._test_signtx("all", self.full_type in supports_multisig) + self._test_signtx("all", self.full_type in supports_multisig, self.full_type in supports_external) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): From c9107b03c9da1b2a391c9ac674400247071c6caf Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 20:47:57 -0500 Subject: [PATCH 027/634] docs: Update documentation w.r.t multisig --- README.md | 2 +- docs/coldcard.md | 4 +++- docs/trezor.md | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7cdfdf2b..631d00a37 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Please also see [docs](docs/) for additional information about each device. | P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | No | Yes | +| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | | Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | diff --git a/docs/coldcard.md b/docs/coldcard.md index 236f9f8c2..d90fa3541 100644 --- a/docs/coldcard.md +++ b/docs/coldcard.md @@ -24,4 +24,6 @@ The `backup` command will create a backup file in the current working directory. ## Caveat for `signtx` -The Coldcard firmware only supports signing single key transactions. It cannot sign multisig or arbitrary scripts yet. +- The Coldcard firmware only supports signing single key and multisig transactions. It cannot sign arbitrary scripts. +- Multsigs need to be registered on the device before a transaction spending that multisig will be signed by the device. +- Multisigs must use BIP 67. This can be accomplished in Bitcoin Core using the `sortedmulti()` descriptor, available in Bitcoin Core 0.20. diff --git a/docs/trezor.md b/docs/trezor.md index 82fe79020..f8ff06df1 100644 --- a/docs/trezor.md +++ b/docs/trezor.md @@ -20,6 +20,7 @@ Due to the limitations of the Trezor, some transactions cannot be signed by a Tr - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. * Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. * Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. +- For **Trezor T**, a transaction cannot contain both segwit and non-segwit inputs ## Note on `backup` From 01ca0e514c9872cdc36a30d1d46cd6ac92361acb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 23 Dec 2019 21:20:38 -0500 Subject: [PATCH 028/634] Replace distutils.core with setuptools in setup.py Poetry geneates a setup.py with distutils.core for the sdist package, which is fine for installing with pip as pip will use setuptools instead of distutils.core. But when people install from source using the setup.py, distutils.core will not install the dependencies; we need setuptools for that. So in our generated setup.py file, replace distutils.core with setuptools. --- contrib/generate_setup.sh | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh index 8786487ba..5e944761a 100755 --- a/contrib/generate_setup.sh +++ b/contrib/generate_setup.sh @@ -30,3 +30,4 @@ tar -xf $tarball $toextract mv $toextract . dir=`echo $toextract | cut -f1 -d"/"` rm -r $dir +sed -i 's/distutils.core/setuptools/g' setup.py diff --git a/setup.py b/setup.py index 6972cbbc8..7da501139 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup packages = \ ['hwilib', From d6829c09e95fc0c8e18674dfcc3ca2e9d8601a2f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 23 Dec 2019 21:30:54 -0500 Subject: [PATCH 029/634] Update README with more installation documentation --- README.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c7cdfdf2b..c1471ddda 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Python 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for For Ubuntu/Debian: ``` -sudo apt install libusb-1.0-0-dev libudev-dev +sudo apt install libusb-1.0-0-dev libudev-dev python3-dev ``` For macOS: @@ -20,22 +20,37 @@ For macOS: brew install libusb ``` -This project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. -Once HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory: +## Install + +``` +git clone https://github.com/bitcoin-core/HWI.git +cd HWI +poetry install # or 'pip3 install .' or 'python3 setup.py install' +``` + +This project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory: ``` poetry install ``` -Pip can also be used to install all of the dependencies (in virtualenv or system) required for operation and development. See `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. +Pip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`): -## Install +``` +pip3 install . +``` + +The `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed: ``` -git clone https://github.com/bitcoin-core/HWI.git -cd HWI +pip3 install -U setuptools +python3 setup.py install ``` +## Dependencies + +See `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods. + ## Usage To use, first enumerate all devices and find the one that you want to use with From 562e736c65a15fd50bb4a7dc4d3679d092671ee8 Mon Sep 17 00:00:00 2001 From: Jeff Frontz Date: Fri, 3 Jan 2020 11:42:14 -0500 Subject: [PATCH 030/634] Add dependencies for Centos --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c1471ddda..b53d986c4 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ For Ubuntu/Debian: sudo apt install libusb-1.0-0-dev libudev-dev python3-dev ``` +For Centos: +``` +sudo yum -y install python3-devel libusbx-devel systemd-devel +``` + For macOS: ``` brew install libusb From 85f677694bf2b827180d52b44fd6d6842dc003d2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 7 Jan 2020 14:53:08 -0500 Subject: [PATCH 031/634] travis: remove pyqt5 dependency we don't need it --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 01214e61d..7493420de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ before_install: mv travis-wait-enhanced /home/travis/bin/ travis-wait-enhanced --version install: - - pip install pipenv pysdl2 protobuf poetry pyqt5 construct mnemonic pyelftools + - pip install pipenv pysdl2 protobuf poetry construct mnemonic pyelftools # 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 From 58cc6bc7106e1c78f89d0dbc3ac3eb79f306f976 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 7 Jan 2020 13:18:25 -0500 Subject: [PATCH 032/634] coldcard: Fix needs_pin_sent and needs_passphrase_sent needs_pin_sent was missing and needs_passphrase_sent was misnamed as needs_passphrase --- hwilib/devices/coldcard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index f6cebd822..fb25b4e94 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -246,7 +246,8 @@ def enumerate(password=''): d_data['type'] = 'coldcard' d_data['model'] = 'coldcard' d_data['path'] = path - d_data['needs_passphrase'] = False + d_data['needs_pin_sent'] = False + d_data['needs_passphrase_sent'] = False client = None with handle_errors(common_err_msgs["enumerate"], d_data): From 0dde0992a823675a81d723b72cacefc2587e8556 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 7 Jan 2020 13:41:37 -0500 Subject: [PATCH 033/634] coldcard: handle simulator enumerate with real devices enumerate Instead of a separate entire special case enumeration for the simulator, do it together with normal device enumeration and just skip it when the simulator not found error shows up. --- hwilib/devices/coldcard.py | 39 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index fb25b4e94..b27f5be4f 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -239,7 +239,9 @@ def send_pin(self, pin): def enumerate(password=''): results = [] - for d in hid.enumerate(COINKITE_VID, CKCC_PID): + devices = hid.enumerate(COINKITE_VID, CKCC_PID) + devices.append({'path': CC_SIMULATOR_SOCK.encode()}) + for d in devices: d_data = {} path = d['path'].decode() @@ -249,35 +251,24 @@ def enumerate(password=''): d_data['needs_pin_sent'] = False d_data['needs_passphrase_sent'] = False + if path == CC_SIMULATOR_SOCK: + d_data['model'] += '_simulator' + client = None with handle_errors(common_err_msgs["enumerate"], d_data): - client = ColdcardClient(path) - d_data['fingerprint'] = client._get_fingerprint_hex() + try: + client = ColdcardClient(path) + d_data['fingerprint'] = client._get_fingerprint_hex() + except RuntimeError as e: + # Skip the simulator if it's not there + if str(e) == 'Cannot connect to simulator. Is it running?': + continue + else: + raise e if client: client.close() results.append(d_data) - # Check if the simulator is there - client = None - try: - client = ColdcardClient(CC_SIMULATOR_SOCK) - - d_data = {} - d_data['fingerprint'] = client._get_fingerprint_hex() - d_data['type'] = 'coldcard' - d_data['model'] = 'coldcard_simulator' - d_data['path'] = CC_SIMULATOR_SOCK - d_data['needs_pin_sent'] = False - d_data['needs_passphrase_sent'] = False - results.append(d_data) - except RuntimeError as e: - if str(e) == 'Cannot connect to simulator. Is it running?': - pass - else: - raise e - if client: - client.close() - return results From 2196f515448aef9d125618feb32c226c69be792c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 7 Jan 2020 13:42:35 -0500 Subject: [PATCH 034/634] ledger: handle simulator enumerate with normal devices Instead of a special case enumeration for the simulator, handle it together with the normal enumeration code and just skip it if there is an exception. --- hwilib/devices/ledger.py | 68 +++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 4fc24c25f..ad9e3866b 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -349,52 +349,42 @@ def send_pin(self, pin): def enumerate(password=''): results = [] + devices = [] for device_id in LEDGER_DEVICE_IDS: - for d in hid.enumerate(LEDGER_VENDOR_ID, device_id): - if ('interface_number' in d and d['interface_number'] == 0 - or ('usage_page' in d and d['usage_page'] == 0xffa0)): - d_data = {} - - path = d['path'].decode() - d_data['type'] = 'ledger' - d_data['model'] = 'ledger_nano_x' if device_id == 0x0004 else 'ledger_nano_s' - d_data['path'] = path - - client = None - with handle_errors(common_err_msgs["enumerate"], d_data): + devices.extend(hid.enumerate(LEDGER_VENDOR_ID, device_id)) + devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 1}) + + for d in devices: + if ('interface_number' in d and d['interface_number'] == 0 + or ('usage_page' in d and d['usage_page'] == 0xffa0)): + d_data = {} + + path = d['path'].decode() + d_data['type'] = 'ledger' + d_data['model'] = 'ledger_nano_x' if d['product_id'] == 0x0004 else 'ledger_nano_s' + d_data['path'] = path + + if path == SIMULATOR_PATH: + d_data['model'] += '_simulator' + + client = None + with handle_errors(common_err_msgs["enumerate"], d_data): + try: client = LedgerClient(path, password) master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub) d_data['needs_pin_sent'] = False d_data['needs_passphrase_sent'] = False + except BTChipException: + # Ignore simulator if there's an exception, means it isn't there + if path == SIMULATOR_PATH: + continue + else: + raise - if client: - client.close() - - results.append(d_data) - - # Check if the simulator is there - client = None - try: - client = LedgerClient(SIMULATOR_PATH, password) - - d_data = {} - d_data['type'] = 'ledger' - d_data['model'] = 'ledger_nano_s_simulator' - d_data['path'] = SIMULATOR_PATH - d_data['needs_pin_sent'] = False - d_data['needs_passphrase_sent'] = False - - master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] - d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub) - d_data['needs_pin_sent'] = False - d_data['needs_passphrase_sent'] = False - - results.append(d_data) - except BTChipException: - pass + if client: + client.close() - if client: - client.close() + results.append(d_data) return results From fd21d404f411247592b3b262f173e147683a32ff Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 7 Jan 2020 13:21:06 -0500 Subject: [PATCH 035/634] test: Check all fields are in enumerate result --- test/test_device.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_device.py b/test/test_device.py index eb97b80bc..5891b8568 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -125,7 +125,13 @@ def test_enumerate(self): found = False for device in enum_res: if (device['type'] == self.type or device['model'] == self.type) and device['path'] == self.path and device['fingerprint'] == self.fingerprint: + self.assertIn('type', device) + self.assertIn('model', device) + self.assertIn('path', device) + self.assertIn('needs_pin_sent', device) + self.assertIn('needs_passphrase_sent', device) self.assertNotIn('error', device) + self.assertNotIn('code', device) found = True self.assertTrue(found) From 52628847d4765ae7f7bc5475a8e4caab27089a13 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 14:22:33 -0500 Subject: [PATCH 036/634] Add PySide2 as a dependency --- poetry.lock | 25 +++++++++++++++++++++++-- pyproject.toml | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 56754c461..b15464142 100644 --- a/poetry.lock +++ b/poetry.lock @@ -162,6 +162,17 @@ version = "3.5" altgraph = "*" setuptools = "*" +[[package]] +category = "main" +description = "Python bindings for the Qt cross-platform application and UI framework" +name = "pyside2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" +version = "5.14.0" + +[package.dependencies] +shiboken2 = "5.14.0" + [[package]] category = "dev" description = "" @@ -171,6 +182,14 @@ optional = false python-versions = "*" version = "0.2.0" +[[package]] +category = "main" +description = "Python / C++ bindings helper module" +name = "shiboken2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" +version = "5.14.0" + [[package]] category = "main" description = "Type Hints for Python" @@ -191,8 +210,8 @@ version = "3.7.4" typing = ">=3.7.4" [metadata] -content-hash = "efb94ab72596b3a8e2c057d24c9ff91efcbe40bfed6b7ca1ed3de909745a72c6" -python-versions = "^3.6" +content-hash = "e076be1de3ff88d71f7ed62865dc7f4ecc424ef5d409cc1b7db578021961164a" +python-versions = "^3.6,<3.9" [metadata.hashes] altgraph = ["d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", "ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"] @@ -212,6 +231,8 @@ pyaes = ["02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pyinstaller = ["ee7504022d1332a3324250faf2135ea56ac71fdb6309cff8cd235de26b1d0a96"] +pyside2 = ["11bba54a62bcd9d7879d3e74cc54c0054c8c6dcdf011ecee9b47c5229cbd7af9", "578b727a5a254cfd509ea2f1fa31779f217a2a1d765c770727662dac950d60eb", "72feeb655958791383085bcb3154f6b3e193c1d66b6aa771c4244a6cafd62b7e", "77474e11c0bb3efa2d7e8506fe0f36049585ba911b8242e070b5f8978e5ba6f7", "c9f59e8c49a9a3b0cca04d8468becd8a562eb9ad0ac1d4d9a8622d2dfa3ce4c9", "ce43f98333443242cd3fe976d72fcb3acf6bb7fa40dd5949e59947a501d5dd72"] pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] +shiboken2 = ["101fc366798f88cbff13586907f3755bebbd9304e66626fb6b0f6b28e0c9a5d2", "47c7c2652f578b37588e8b6daff3a852b3c88ae0f83be13886e4a74859e81763", "4f138656fc755399776062c89492d61f887d4e5fe7c78cded73917e80afcf2f5", "676fef81e4d95b02816fde7359c1f2604efa3edd34b05ab0da42c57c7555f7d7", "a88267c7cc17501effc6b1b36d85e7ab28173af939b975ea42716ed12493b478", "ab3ba84784c9641a11a21a8c64d494fa8b57be25e081e77f76747d543699f03c"] typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] diff --git a/pyproject.toml b/pyproject.toml index 9f2d7e7b1..d5670d33a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,13 +15,14 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.6" +python = "^3.6,<3.9" hidapi = "^0.7.99" ecdsa = "^0.13.0" pyaes = "^1.6" mnemonic = "^0.18.0" typing-extensions = "^3.7" libusb1 = "^1.7" +pyside2 = "^5.14.0" [tool.poetry.dev-dependencies] pyinstaller = "^3.4" From 982aabcb2558c560f62d12cc25d58b7018e5d7d9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 14:24:38 -0500 Subject: [PATCH 037/634] Initial Qt Creator and UI files .gitignore is updated to ignore some things --- .flake8 | 2 +- .gitignore | 4 ++++ hwilib/ui/hwiqt.pyproject | 3 +++ hwilib/ui/mainwindow.ui | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 hwilib/ui/hwiqt.pyproject create mode 100644 hwilib/ui/mainwindow.ui diff --git a/.flake8 b/.flake8 index d2decef36..568730700 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -exclude = *.pyc,__pycache__,hwilib/devices/btchip/,hwilib/devices/ckcc/,hwilib/devices/trezorlib/,test/work/ +exclude = *.pyc,__pycache__,hwilib/devices/btchip/,hwilib/devices/ckcc/,hwilib/devices/trezorlib/,test/work/,hwilib/ui ignore = E261,E302,E305,E501,E722,W5 per-file-ignores = setup.py:E122 diff --git a/.gitignore b/.gitignore index 091dd55d7..8a4822368 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ test/emulator.img test/work pip-wheel-metadata .mypy_cache/ + +# Qt stuff +hwiqt.pyproject.user +hwilib/ui/ui_*.py diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject new file mode 100644 index 000000000..7014cff18 --- /dev/null +++ b/hwilib/ui/hwiqt.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["mainwindow.ui"] +} diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui new file mode 100644 index 000000000..e5d71a939 --- /dev/null +++ b/hwilib/ui/mainwindow.ui @@ -0,0 +1,33 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 19 + 29 + 751 + 531 + + + + + + + + + + From e3522989fb903a7da423f2e7383e592904ae1d9c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 14:44:32 -0500 Subject: [PATCH 038/634] Add Script to generate ui*.py files --- contrib/generate-ui.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 contrib/generate-ui.sh diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh new file mode 100755 index 000000000..349b6a544 --- /dev/null +++ b/contrib/generate-ui.sh @@ -0,0 +1,10 @@ +#! /bin/bash + +pushd hwilib/ui +for file in *.ui +do + gen_file=ui_`echo $file| cut -d. -f1`.py + pyside2-uic $file -o $gen_file + sed -i 's/raise()/raise_()/g' $gen_file +done +popd From a5ca3765f430383f82dc1275a19ee95342054b7a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 14:44:46 -0500 Subject: [PATCH 039/634] Initial HWI Qt program --- hwi-qt.py | 7 +++++++ hwilib/gui.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100755 hwi-qt.py create mode 100644 hwilib/gui.py diff --git a/hwi-qt.py b/hwi-qt.py new file mode 100755 index 000000000..edf24b359 --- /dev/null +++ b/hwi-qt.py @@ -0,0 +1,7 @@ +#! /usr/bin/env python3 + +if __name__ == '__main__': + from hwilib.gui import main + main() +else: + raise ImportError('hwi-qt is not importable. Import hwilib instead') diff --git a/hwilib/gui.py b/hwilib/gui.py new file mode 100644 index 000000000..046316698 --- /dev/null +++ b/hwilib/gui.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python3 + +from . import commands + +try: + from .ui.ui_mainwindow import Ui_MainWindow +except ImportError: + print('Could not import UI files, did you run contrib/generate-ui.sh') + exit(-1) + +from PySide2.QtWidgets import QApplication, QMainWindow + +class HWIQt(QMainWindow): + def __init__(self): + super(HWIQt, self).__init__() + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + self.setWindowTitle('HWI Qt') + +def main(): + devices = commands.enumerate() + print(devices) + + app = QApplication() + + window = HWIQt() + window.show() + + app.exec_() From be5ce05692d543922705627a1b7ce74e056b38b1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 15:08:38 -0500 Subject: [PATCH 040/634] Implement device chooser and refresh button --- hwilib/gui.py | 23 ++++++++++++--- hwilib/ui/mainwindow.ui | 62 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index 046316698..89421cfdb 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -9,6 +9,7 @@ exit(-1) from PySide2.QtWidgets import QApplication, QMainWindow +from PySide2.QtCore import Slot class HWIQt(QMainWindow): def __init__(self): @@ -17,13 +18,27 @@ def __init__(self): self.ui.setupUi(self) self.setWindowTitle('HWI Qt') -def main(): - devices = commands.enumerate() - print(devices) + self.devices = [] + + self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) + @Slot() + def refresh_clicked(self): + self.devices = commands.enumerate() + self.ui.enumerate_combobox.clear() + for dev in self.devices: + fingerprint = 'none' + if 'fingerprint' in dev: + fingerprint = dev['fingerprint'] + dev_str = '{} fingerprint:{} path:{}'.format(dev['model'], fingerprint, dev['path']) + self.ui.enumerate_combobox.addItem(dev_str) + +def main(): app = QApplication() window = HWIQt() - window.show() + window.refresh_clicked() + + window.show() app.exec_() diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui index e5d71a939..87ee508ea 100644 --- a/hwilib/ui/mainwindow.ui +++ b/hwilib/ui/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 828 600 @@ -19,11 +19,67 @@ 19 29 - 751 + 794 531 - + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 200 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 100 + 30 + + + + Refresh + + + + + + From 855f34aa2c5c2c3a107064504685b49762d3429a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 15:27:01 -0500 Subject: [PATCH 041/634] Add action buttons and keypool and descriptor display boxes Just adds the buttons to the UI, they don't do anything. --- hwilib/ui/mainwindow.ui | 122 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui index 87ee508ea..4de441ea3 100644 --- a/hwilib/ui/mainwindow.ui +++ b/hwilib/ui/mainwindow.ui @@ -7,7 +7,7 @@ 0 0 828 - 600 + 430 @@ -20,7 +20,7 @@ 19 29 794 - 531 + 375 @@ -79,6 +79,124 @@ + + + + Qt::Horizontal + + + + + + + + + Send Pin + + + + + + + Set Passphrase + + + + + + + Sign PSBT + + + + + + + Get an xpub + + + + + + + Sign Message + + + + + + + + 0 + 0 + + + + + 100 + 20 + + + + Actions: + + + + + + + Change the options used for getkeypool + + + Change getkeypool options + + + + + + + Display Address + + + + + + + + + + + Keypool: + + + + + + + true + + + + + + + + + + + Descriptors: + + + + + + + true + + + + + From ac59af82eeaf71908502837b381d0e4c70a5ff6c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 16:24:15 -0500 Subject: [PATCH 042/634] Add SetPassphraseDialog triggered by setpass_button --- hwilib/gui.py | 30 +++++++++++- hwilib/ui/hwiqt.pyproject | 2 +- hwilib/ui/setpassphrasedialog.ui | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 hwilib/ui/setpassphrasedialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index 89421cfdb..792dc70db 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -4,13 +4,23 @@ try: from .ui.ui_mainwindow import Ui_MainWindow + from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog except ImportError: print('Could not import UI files, did you run contrib/generate-ui.sh') exit(-1) -from PySide2.QtWidgets import QApplication, QMainWindow +from PySide2.QtWidgets import QApplication, QDialog, QMainWindow from PySide2.QtCore import Slot +class SetPassphraseDialog(QDialog): + def __init__(self): + super(SetPassphraseDialog, self).__init__() + self.ui = Ui_SetPassphraseDialog() + self.ui.setupUi(self) + self.setWindowTitle('Set Passphrase') + + self.ui.passphrase_lineedit.setFocus() + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -19,13 +29,18 @@ def __init__(self): self.setWindowTitle('HWI Qt') self.devices = [] + self.client = None + self.passphrase = '' + self.current_dialog = None self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) + self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) @Slot() def refresh_clicked(self): - self.devices = commands.enumerate() + self.devices = commands.enumerate(self.passphrase) self.ui.enumerate_combobox.clear() + self.ui.enumerate_combobox.addItem('') for dev in self.devices: fingerprint = 'none' if 'fingerprint' in dev: @@ -33,6 +48,17 @@ def refresh_clicked(self): dev_str = '{} fingerprint:{} path:{}'.format(dev['model'], fingerprint, dev['path']) self.ui.enumerate_combobox.addItem(dev_str) + @Slot() + def show_setpassphrasedialog(self): + self.current_dialog = SetPassphraseDialog() + self.current_dialog.accepted.connect(self.setpassphrasedialog_accepted) + self.current_dialog.exec_() + + @Slot() + def setpassphrasedialog_accepted(self): + self.passphrase = self.current_dialog.ui.passphrase_lineedit.text() + self.current_dialog = None + def main(): app = QApplication() diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index 7014cff18..1ef4c5de6 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui"] + "files": ["mainwindow.ui","setpassphrasedialog.ui"] } diff --git a/hwilib/ui/setpassphrasedialog.ui b/hwilib/ui/setpassphrasedialog.ui new file mode 100644 index 000000000..fe26a2639 --- /dev/null +++ b/hwilib/ui/setpassphrasedialog.ui @@ -0,0 +1,81 @@ + + + SetPassphraseDialog + + + + 0 + 0 + 400 + 96 + + + + Dialog + + + + + 40 + 50 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 30 + 10 + 351 + 32 + + + + false + + + + + + + buttonBox + accepted() + SetPassphraseDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SetPassphraseDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 21b92be0885336bba9c7d2f22144383cfa4a745d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 17:08:02 -0500 Subject: [PATCH 043/634] Show getkeypool and getdescriptor info when device selected --- hwilib/gui.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/hwilib/gui.py b/hwilib/gui.py index 792dc70db..818bced8b 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -1,5 +1,7 @@ #! /usr/bin/env python3 +import json + from . import commands try: @@ -36,9 +38,12 @@ def __init__(self): self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) + self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_device_info) + @Slot() def refresh_clicked(self): self.devices = commands.enumerate(self.passphrase) + self.ui.enumerate_combobox.currentIndexChanged.disconnect() self.ui.enumerate_combobox.clear() self.ui.enumerate_combobox.addItem('') for dev in self.devices: @@ -47,6 +52,7 @@ def refresh_clicked(self): fingerprint = dev['fingerprint'] dev_str = '{} fingerprint:{} path:{}'.format(dev['model'], fingerprint, dev['path']) self.ui.enumerate_combobox.addItem(dev_str) + self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_device_info) @Slot() def show_setpassphrasedialog(self): @@ -59,6 +65,21 @@ def setpassphrasedialog_accepted(self): self.passphrase = self.current_dialog.ui.passphrase_lineedit.text() self.current_dialog = None + @Slot() + def get_device_info(self, index): + if index == 0: + return + # Get the client + dev = self.devices[index - 1] + self.client = commands.get_client(dev['model'], dev['path'], self.passphrase) + + # do getkeypool and getdescriptors + keypool = commands.getkeypool(self.client, 'm/49h/0h/0h/*', 0, 1000, False, True, 0, False, True) + descriptors = commands.getdescriptors(self.client, 0) + + self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) + self.ui.desc_textedit.setPlainText(json.dumps(descriptors, indent=2)) + def main(): app = QApplication() From 5bced4f96fd13143b15a0038bead5d67c22bd8ea Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 17:19:59 -0500 Subject: [PATCH 044/634] Change sendpin_button enabled-ness depending of trezor --- hwilib/gui.py | 9 +++++++++ hwilib/ui/mainwindow.ui | 3 +++ 2 files changed, 12 insertions(+) diff --git a/hwilib/gui.py b/hwilib/gui.py index 818bced8b..378b3a348 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -67,12 +67,21 @@ def setpassphrasedialog_accepted(self): @Slot() def get_device_info(self, index): + self.ui.sendpin_button.setEnabled(False) if index == 0: return + # Get the client dev = self.devices[index - 1] self.client = commands.get_client(dev['model'], dev['path'], self.passphrase) + # Enable the sendpin button if it's a trezor and it needs it + if dev['needs_pin_sent']: + self.ui.sendpin_button.setEnabled(True) + return + else: + self.ui.sendpin_button.setEnabled(False) + # do getkeypool and getdescriptors keypool = commands.getkeypool(self.client, 'm/49h/0h/0h/*', 0, 1000, False, True, 0, False, True) descriptors = commands.getdescriptors(self.client, 0) diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui index 4de441ea3..7748bfde6 100644 --- a/hwilib/ui/mainwindow.ui +++ b/hwilib/ui/mainwindow.ui @@ -90,6 +90,9 @@ + + false + Send Pin From 3d530cad2d2b1d9e51c7ba20ee55fdb30271bc7e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 16 Dec 2019 18:06:01 -0500 Subject: [PATCH 045/634] Implement SendPinDialog and sendpin_button --- hwilib/gui.py | 82 +++++++++++++++++-- hwilib/ui/hwiqt.pyproject | 2 +- hwilib/ui/sendpindialog.ui | 156 +++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 hwilib/ui/sendpindialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index 378b3a348..e01ea9ffb 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -6,13 +6,15 @@ try: from .ui.ui_mainwindow import Ui_MainWindow + from .ui.ui_sendpindialog import Ui_SendPinDialog from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog except ImportError: print('Could not import UI files, did you run contrib/generate-ui.sh') exit(-1) -from PySide2.QtWidgets import QApplication, QDialog, QMainWindow -from PySide2.QtCore import Slot +from PySide2.QtGui import QRegExpValidator +from PySide2.QtWidgets import QApplication, QDialog, QLineEdit, QMainWindow +from PySide2.QtCore import QRegExp, Signal, Slot class SetPassphraseDialog(QDialog): def __init__(self): @@ -23,6 +25,48 @@ def __init__(self): self.ui.passphrase_lineedit.setFocus() +class SendPinDialog(QDialog): + pin_sent_success = Signal() + + def __init__(self, client): + super(SendPinDialog, self).__init__() + self.ui = Ui_SendPinDialog() + self.ui.setupUi(self) + self.setWindowTitle('Send Pin') + self.client = client + self.ui.pin_lineedit.setFocus() + self.ui.pin_lineedit.setValidator(QRegExpValidator(QRegExp("[1-9]+"), None)) + self.ui.pin_lineedit.setEchoMode(QLineEdit.Password) + + self.ui.p1_button.clicked.connect(self.button_clicked(1)) + self.ui.p2_button.clicked.connect(self.button_clicked(2)) + self.ui.p3_button.clicked.connect(self.button_clicked(3)) + self.ui.p4_button.clicked.connect(self.button_clicked(4)) + self.ui.p5_button.clicked.connect(self.button_clicked(5)) + self.ui.p6_button.clicked.connect(self.button_clicked(6)) + self.ui.p7_button.clicked.connect(self.button_clicked(7)) + self.ui.p8_button.clicked.connect(self.button_clicked(8)) + self.ui.p9_button.clicked.connect(self.button_clicked(9)) + + self.accepted.connect(self.sendpindialog_accepted) + commands.prompt_pin(self.client) + + def button_clicked(self, number): + @Slot() + def button_clicked_num(): + self.ui.pin_lineedit.setText(self.ui.pin_lineedit.text() + str(number)) + return button_clicked_num + + @Slot() + def sendpindialog_accepted(self): + pin = self.ui.pin_lineedit.text() + + # Send the pin + commands.send_pin(self.client, pin) + self.client.close() + self.client = None + self.pin_sent_success.emit() + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -32,16 +76,22 @@ def __init__(self): self.devices = [] self.client = None + self.device_info = {} self.passphrase = '' self.current_dialog = None self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) + self.ui.sendpin_button.clicked.connect(self.show_sendpindialog) - self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_device_info) + self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @Slot() def refresh_clicked(self): + if self.client: + self.client.close() + self.client = None + self.devices = commands.enumerate(self.passphrase) self.ui.enumerate_combobox.currentIndexChanged.disconnect() self.ui.enumerate_combobox.clear() @@ -52,7 +102,7 @@ def refresh_clicked(self): fingerprint = dev['fingerprint'] dev_str = '{} fingerprint:{} path:{}'.format(dev['model'], fingerprint, dev['path']) self.ui.enumerate_combobox.addItem(dev_str) - self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_device_info) + self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @Slot() def show_setpassphrasedialog(self): @@ -66,17 +116,19 @@ def setpassphrasedialog_accepted(self): self.current_dialog = None @Slot() - def get_device_info(self, index): + def get_client_and_device_info(self, index): self.ui.sendpin_button.setEnabled(False) if index == 0: return # Get the client - dev = self.devices[index - 1] - self.client = commands.get_client(dev['model'], dev['path'], self.passphrase) + self.device_info = self.devices[index - 1] + self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) + self.get_device_info() + def get_device_info(self): # Enable the sendpin button if it's a trezor and it needs it - if dev['needs_pin_sent']: + if self.device_info['needs_pin_sent']: self.ui.sendpin_button.setEnabled(True) return else: @@ -89,6 +141,20 @@ def get_device_info(self, index): self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) self.ui.desc_textedit.setPlainText(json.dumps(descriptors, indent=2)) + @Slot() + def show_sendpindialog(self): + self.current_dialog = SendPinDialog(self.client) + self.current_dialog.pin_sent_success.connect(self.sendpindialog_accepted) + self.current_dialog.exec_() + + @Slot() + def sendpindialog_accepted(self): + self.current_dialog = None + + curr_index = self.ui.enumerate_combobox.currentIndex() + self.refresh_clicked() + self.ui.enumerate_combobox.setCurrentIndex(curr_index) + def main(): app = QApplication() diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index 1ef4c5de6..a3bca8a38 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui","setpassphrasedialog.ui"] + "files": ["mainwindow.ui","setpassphrasedialog.ui","sendpindialog.ui"] } diff --git a/hwilib/ui/sendpindialog.ui b/hwilib/ui/sendpindialog.ui new file mode 100644 index 000000000..83d691723 --- /dev/null +++ b/hwilib/ui/sendpindialog.ui @@ -0,0 +1,156 @@ + + + SendPinDialog + + + + 0 + 0 + 257 + 234 + + + + Dialog + + + + + 60 + 190 + 181 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 10 + 10 + 231 + 32 + + + + false + + + + + + 10 + 50 + 231 + 131 + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + ? + + + + + + + + + + buttonBox + accepted() + SendPinDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SendPinDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 779eced176a7ef6573a8d5618d9c5683f5c1e9c5 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 13:24:05 -0500 Subject: [PATCH 046/634] Add GetXpubDialog --- hwilib/gui.py | 30 +++++++++- hwilib/ui/getxpubdialog.ui | 115 +++++++++++++++++++++++++++++++++++++ hwilib/ui/hwiqt.pyproject | 2 +- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 hwilib/ui/getxpubdialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index e01ea9ffb..e04a7f183 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -5,6 +5,7 @@ from . import commands try: + from .ui.ui_getxpubdialog import Ui_GetXpubDialog from .ui.ui_mainwindow import Ui_MainWindow from .ui.ui_sendpindialog import Ui_SendPinDialog from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog @@ -13,7 +14,7 @@ exit(-1) from PySide2.QtGui import QRegExpValidator -from PySide2.QtWidgets import QApplication, QDialog, QLineEdit, QMainWindow +from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMainWindow from PySide2.QtCore import QRegExp, Signal, Slot class SetPassphraseDialog(QDialog): @@ -67,6 +68,27 @@ def sendpindialog_accepted(self): self.client = None self.pin_sent_success.emit() +class GetXpubDialog(QDialog): + def __init__(self, client): + super(GetXpubDialog, self).__init__() + self.ui = Ui_GetXpubDialog() + self.ui.setupUi(self) + self.setWindowTitle('Get xpub') + self.client = client + + self.ui.path_lineedit.setValidator(QRegExpValidator(QRegExp("m(/[0-9]+['Hh]?)+"), None)) + self.ui.path_lineedit.setFocus() + self.ui.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False) + + self.ui.getxpub_button.clicked.connect(self.getxpub_button_clicked) + self.ui.buttonBox.clicked.connect(self.accept) + + @Slot() + def getxpub_button_clicked(self): + path = self.ui.path_lineedit.text() + res = commands.getxpub(self.client, path) + self.ui.xpub_lineedit.setText(res['xpub']) + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -83,6 +105,7 @@ def __init__(self): self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) self.ui.sendpin_button.clicked.connect(self.show_sendpindialog) + self.ui.getxpub_button.clicked.connect(self.show_getxpubdialog) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -155,6 +178,11 @@ def sendpindialog_accepted(self): self.refresh_clicked() self.ui.enumerate_combobox.setCurrentIndex(curr_index) + @Slot() + def show_getxpubdialog(self): + self.current_dialog = GetXpubDialog(self.client) + self.current_dialog.exec_() + def main(): app = QApplication() diff --git a/hwilib/ui/getxpubdialog.ui b/hwilib/ui/getxpubdialog.ui new file mode 100644 index 000000000..0d6417224 --- /dev/null +++ b/hwilib/ui/getxpubdialog.ui @@ -0,0 +1,115 @@ + + + GetXpubDialog + + + + 0 + 0 + 1205 + 158 + + + + Dialog + + + + + 320 + 20 + 101 + 31 + + + + Derivation Path + + + + + + 430 + 20 + 401 + 32 + + + + + + + 1100 + 110 + 91 + 34 + + + + Qt::NoFocus + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + 840 + 20 + 88 + 34 + + + + Get xpub + + + false + + + true + + + + + + 30 + 70 + 41 + 31 + + + + xpub + + + + + + 70 + 70 + 1121 + 32 + + + + Qt::NoFocus + + + true + + + buttonBox + path_label + path_lineedit + getxpub_button + xpub_label + xpub_lineedit + + + + diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index a3bca8a38..24eeca494 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui","setpassphrasedialog.ui","sendpindialog.ui"] + "files": ["mainwindow.ui","setpassphrasedialog.ui","sendpindialog.ui","getxpubdialog.ui"] } From eea7517bdb2db5bf13b0401066cc283e7fa48b31 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 13:57:27 -0500 Subject: [PATCH 047/634] Add SignPSBTDialog --- hwilib/gui.py | 26 +++++++ hwilib/ui/hwiqt.pyproject | 2 +- hwilib/ui/signpsbtdialog.ui | 142 ++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 hwilib/ui/signpsbtdialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index e04a7f183..5159c7b85 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -9,6 +9,7 @@ from .ui.ui_mainwindow import Ui_MainWindow from .ui.ui_sendpindialog import Ui_SendPinDialog from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog + from .ui.ui_signpsbtdialog import Ui_SignPSBTDialog except ImportError: print('Could not import UI files, did you run contrib/generate-ui.sh') exit(-1) @@ -89,6 +90,25 @@ def getxpub_button_clicked(self): res = commands.getxpub(self.client, path) self.ui.xpub_lineedit.setText(res['xpub']) +class SignPSBTDialog(QDialog): + def __init__(self, client): + super(SignPSBTDialog, self).__init__() + self.ui = Ui_SignPSBTDialog() + self.ui.setupUi(self) + self.setWindowTitle('Sign PSBT') + self.client = client + + self.ui.psbt_in_textedit.setFocus() + + self.ui.sign_psbt_button.clicked.connect(self.sign_psbt_button_clicked) + self.ui.buttonBox.clicked.connect(self.accept) + + @Slot() + def sign_psbt_button_clicked(self): + psbt_str = self.ui.psbt_in_textedit.toPlainText() + res = commands.signtx(self.client, psbt_str) + self.ui.psbt_out_textedit.setPlainText(res['psbt']) + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -106,6 +126,7 @@ def __init__(self): self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) self.ui.sendpin_button.clicked.connect(self.show_sendpindialog) self.ui.getxpub_button.clicked.connect(self.show_getxpubdialog) + self.ui.signtx_button.clicked.connect(self.show_signpsbtdialog) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -183,6 +204,11 @@ def show_getxpubdialog(self): self.current_dialog = GetXpubDialog(self.client) self.current_dialog.exec_() + @Slot() + def show_signpsbtdialog(self): + self.current_dialog = SignPSBTDialog(self.client) + self.current_dialog.exec_() + def main(): app = QApplication() diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index 24eeca494..8ec317aab 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui","setpassphrasedialog.ui","sendpindialog.ui","getxpubdialog.ui"] + "files": ["mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","sendpindialog.ui","signpsbtdialog.ui"] } diff --git a/hwilib/ui/signpsbtdialog.ui b/hwilib/ui/signpsbtdialog.ui new file mode 100644 index 000000000..66b4f3b5b --- /dev/null +++ b/hwilib/ui/signpsbtdialog.ui @@ -0,0 +1,142 @@ + + + SignPSBTDialog + + + + 0 + 0 + 987 + 813 + + + + Dialog + + + + + 630 + 760 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + 20 + 180 + 58 + 61 + + + + PSBT To Sign + + + true + + + + + + 90 + 20 + 881 + 321 + + + + + + + 90 + 410 + 881 + 331 + + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + 30 + 530 + 58 + 61 + + + + PSBT Result + + + true + + + + + + 480 + 350 + 88 + 34 + + + + Sign PSBT + + + false + + + true + + + + + + + buttonBox + accepted() + SignPSBTDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SignPSBTDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 53048e6e6325c1ad4e13b09e86ada84293d9d7fa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 14:06:40 -0500 Subject: [PATCH 048/634] Add SignMessageDialog --- hwilib/gui.py | 28 ++++++ hwilib/ui/hwiqt.pyproject | 2 +- hwilib/ui/signmessagedialog.ui | 159 +++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 hwilib/ui/signmessagedialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index 5159c7b85..5f7d64621 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -9,6 +9,7 @@ from .ui.ui_mainwindow import Ui_MainWindow from .ui.ui_sendpindialog import Ui_SendPinDialog from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog + from .ui.ui_signmessagedialog import Ui_SignMessageDialog from .ui.ui_signpsbtdialog import Ui_SignPSBTDialog except ImportError: print('Could not import UI files, did you run contrib/generate-ui.sh') @@ -109,6 +110,27 @@ def sign_psbt_button_clicked(self): res = commands.signtx(self.client, psbt_str) self.ui.psbt_out_textedit.setPlainText(res['psbt']) +class SignMessageDialog(QDialog): + def __init__(self, client): + super(SignMessageDialog, self).__init__() + self.ui = Ui_SignMessageDialog() + self.ui.setupUi(self) + self.setWindowTitle('Sign Message') + self.client = client + + self.ui.path_lineedit.setValidator(QRegExpValidator(QRegExp("m(/[0-9]+['Hh]?)+"), None)) + self.ui.msg_textedit.setFocus() + + self.ui.signmsg_button.clicked.connect(self.signmsg_button_clicked) + self.ui.buttonBox.clicked.connect(self.accept) + + @Slot() + def signmsg_button_clicked(self): + msg_str = self.ui.msg_textedit.toPlainText() + path = self.ui.path_lineedit.text() + res = commands.signmessage(self.client, msg_str, path) + self.ui.sig_textedit.setPlainText(res['signature']) + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -127,6 +149,7 @@ def __init__(self): self.ui.sendpin_button.clicked.connect(self.show_sendpindialog) self.ui.getxpub_button.clicked.connect(self.show_getxpubdialog) self.ui.signtx_button.clicked.connect(self.show_signpsbtdialog) + self.ui.signmsg_button.clicked.connect(self.show_signmessagedialog) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -209,6 +232,11 @@ def show_signpsbtdialog(self): self.current_dialog = SignPSBTDialog(self.client) self.current_dialog.exec_() + @Slot() + def show_signmessagedialog(self): + self.current_dialog = SignMessageDialog(self.client) + self.current_dialog.exec_() + def main(): app = QApplication() diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index 8ec317aab..21298ec67 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","sendpindialog.ui","signpsbtdialog.ui"] + "files": ["mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","signpsbtdialog.ui","sendpindialog.ui","signmessagedialog.ui"] } diff --git a/hwilib/ui/signmessagedialog.ui b/hwilib/ui/signmessagedialog.ui new file mode 100644 index 000000000..254bbe35d --- /dev/null +++ b/hwilib/ui/signmessagedialog.ui @@ -0,0 +1,159 @@ + + + SignMessageDialog + + + + 0 + 0 + 957 + 350 + + + + Dialog + + + + + 600 + 300 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + 20 + 70 + 61 + 18 + + + + Message + + + + + + 80 + 20 + 861 + 131 + + + + + + + 20 + 180 + 121 + 18 + + + + Key Derivation Path + + + + + + 150 + 170 + 391 + 32 + + + + + + + 570 + 170 + 101 + 34 + + + + Sign Message + + + false + + + true + + + + + + 20 + 230 + 71 + 18 + + + + Signature + + + + + + 90 + 220 + 851 + 61 + + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + buttonBox + accepted() + SignMessageDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SignMessageDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 7760d8e55bac08671b19d665de0a08b27eb3fb84 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 15:24:25 -0500 Subject: [PATCH 049/634] Add DisplayAddressDialog --- hwilib/gui.py | 27 ++++ hwilib/ui/displayaddressdialog.ui | 203 ++++++++++++++++++++++++++++++ hwilib/ui/hwiqt.pyproject | 2 +- 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 hwilib/ui/displayaddressdialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index 5f7d64621..1bfeccc3f 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -5,6 +5,7 @@ from . import commands try: + from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog from .ui.ui_getxpubdialog import Ui_GetXpubDialog from .ui.ui_mainwindow import Ui_MainWindow from .ui.ui_sendpindialog import Ui_SendPinDialog @@ -131,6 +132,26 @@ def signmsg_button_clicked(self): res = commands.signmessage(self.client, msg_str, path) self.ui.sig_textedit.setPlainText(res['signature']) +class DisplayAddressDialog(QDialog): + def __init__(self, client): + super(DisplayAddressDialog, self).__init__() + self.ui = Ui_DisplayAddressDialog() + self.ui.setupUi(self) + self.setWindowTitle('Display Address') + self.client = client + + self.ui.path_lineedit.setValidator(QRegExpValidator(QRegExp("m(/[0-9]+['Hh]?)+"), None)) + self.ui.path_lineedit.setFocus() + + self.ui.go_button.clicked.connect(self.go_button_clicked) + self.ui.buttonBox.clicked.connect(self.accept) + + @Slot() + def go_button_clicked(self): + path = self.ui.path_lineedit.text() + res = commands.displayaddress(self.client, path, sh_wpkh=self.ui.sh_wpkh_radio.isChecked(), wpkh=self.ui.wpkh_radio.isChecked()) + self.ui.address_lineedit.setText(res['address']) + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -150,6 +171,7 @@ def __init__(self): self.ui.getxpub_button.clicked.connect(self.show_getxpubdialog) self.ui.signtx_button.clicked.connect(self.show_signpsbtdialog) self.ui.signmsg_button.clicked.connect(self.show_signmessagedialog) + self.ui.display_addr_button.clicked.connect(self.show_displayaddressdialog) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -237,6 +259,11 @@ def show_signmessagedialog(self): self.current_dialog = SignMessageDialog(self.client) self.current_dialog.exec_() + @Slot() + def show_displayaddressdialog(self): + self.current_dialog = DisplayAddressDialog(self.client) + self.current_dialog.exec_() + def main(): app = QApplication() diff --git a/hwilib/ui/displayaddressdialog.ui b/hwilib/ui/displayaddressdialog.ui new file mode 100644 index 000000000..e427e7d50 --- /dev/null +++ b/hwilib/ui/displayaddressdialog.ui @@ -0,0 +1,203 @@ + + + DisplayAddressDialog + + + + 0 + 0 + 469 + 196 + + + + Dialog + + + + + 350 + 150 + 101 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + 120 + 10 + 331 + 32 + + + + + + + 10 + 20 + 111 + 18 + + + + Derivation Path + + + + + + 410 + 50 + 41 + 41 + + + + Go + + + false + + + true + + + + + + 10 + 120 + 58 + 18 + + + + Address + + + + + + 70 + 110 + 381 + 32 + + + + true + + + + + + 10 + 50 + 381 + 40 + + + + + 0 + 0 + + + + + 0 + 30 + + + + + + + + + 10 + 10 + 121 + 22 + + + + P2SH-P2WPKH + + + true + + + + + + 150 + 10 + 91 + 22 + + + + P2WPKH + + + + + + 260 + 10 + 105 + 22 + + + + P2PKH + + + + + + + + buttonBox + accepted() + DisplayAddressDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DisplayAddressDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index 21298ec67..e210ae06c 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","signpsbtdialog.ui","sendpindialog.ui","signmessagedialog.ui"] + "files": ["signmessagedialog.ui","mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","signpsbtdialog.ui","sendpindialog.ui","displayaddressdialog.ui"] } From 82125a58ac8fd301d93288d0ffcf3513469e0891 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 16:17:23 -0500 Subject: [PATCH 050/634] Add GetKeypoolOptionsDialog --- hwilib/gui.py | 85 +++++++- hwilib/ui/getkeypooloptionsdialog.ui | 281 +++++++++++++++++++++++++++ hwilib/ui/hwiqt.pyproject | 2 +- 3 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 hwilib/ui/getkeypooloptionsdialog.ui diff --git a/hwilib/gui.py b/hwilib/gui.py index 1bfeccc3f..594527404 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -7,6 +7,7 @@ try: from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog from .ui.ui_getxpubdialog import Ui_GetXpubDialog + from .ui.ui_getkeypooloptionsdialog import Ui_GetKeypoolOptionsDialog from .ui.ui_mainwindow import Ui_MainWindow from .ui.ui_sendpindialog import Ui_SendPinDialog from .ui.ui_setpassphrasedialog import Ui_SetPassphraseDialog @@ -152,6 +153,45 @@ def go_button_clicked(self): res = commands.displayaddress(self.client, path, sh_wpkh=self.ui.sh_wpkh_radio.isChecked(), wpkh=self.ui.wpkh_radio.isChecked()) self.ui.address_lineedit.setText(res['address']) +class GetKeypoolOptionsDialog(QDialog): + def __init__(self, opts): + super(GetKeypoolOptionsDialog, self).__init__() + self.ui = Ui_GetKeypoolOptionsDialog() + self.ui.setupUi(self) + self.setWindowTitle('Set getkeypool options') + + self.ui.start_spinbox.setValue(opts['start']) + self.ui.end_spinbox.setValue(opts['end']) + self.ui.internal_checkbox.setChecked(opts['internal']) + self.ui.keypool_checkbox.setChecked(opts['keypool']) + self.ui.account_spinbox.setValue(opts['account']) + self.ui.path_lineedit.setValidator(QRegExpValidator(QRegExp("m(/[0-9]+['Hh]?)+"), None)) + if opts['account_used']: + self.ui.account_radio.setChecked(True) + self.ui.path_radio.setChecked(False) + self.ui.path_lineedit.setEnabled(False) + self.ui.account_spinbox.setEnabled(True) + self.ui.account_spinbox.setValue(opts['account']) + else: + self.ui.account_radio.setChecked(False) + self.ui.path_radio.setChecked(True) + self.ui.path_lineedit.setEnabled(True) + self.ui.account_spinbox.setEnabled(False) + self.ui.path_lineedit.setText(opts['path']) + self.ui.sh_wpkh_radio.setChecked(opts['sh_wpkh']) + self.ui.wpkh_radio.setChecked(opts['wpkh']) + + self.ui.account_radio.toggled.connect(self.toggle_account) + + @Slot() + def toggle_account(self, checked): + if checked: + self.ui.path_lineedit.setEnabled(False) + self.ui.account_spinbox.setEnabled(True) + else: + self.ui.path_lineedit.setEnabled(True) + self.ui.account_spinbox.setEnabled(False) + class HWIQt(QMainWindow): def __init__(self): super(HWIQt, self).__init__() @@ -164,6 +204,17 @@ def __init__(self): self.device_info = {} self.passphrase = '' self.current_dialog = None + self.getkeypool_opts = { + 'start': 0, + 'end': 1000, + 'account': 0, + 'internal': False, + 'keypool': True, + 'sh_wpkh': True, + 'wpkh': False, + 'path': None, + 'account_used': True + } self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) @@ -172,6 +223,7 @@ def __init__(self): self.ui.signtx_button.clicked.connect(self.show_signpsbtdialog) self.ui.signmsg_button.clicked.connect(self.show_signmessagedialog) self.ui.display_addr_button.clicked.connect(self.show_displayaddressdialog) + self.ui.getkeypool_opts_button.clicked.connect(self.show_getkeypooloptionsdialog) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -224,7 +276,15 @@ def get_device_info(self): self.ui.sendpin_button.setEnabled(False) # do getkeypool and getdescriptors - keypool = commands.getkeypool(self.client, 'm/49h/0h/0h/*', 0, 1000, False, True, 0, False, True) + keypool = commands.getkeypool(self.client, + None if self.getkeypool_opts['account_used'] else self.getkeypool_opts['path'], + self.getkeypool_opts['start'], + self.getkeypool_opts['end'], + self.getkeypool_opts['internal'], + self.getkeypool_opts['keypool'], + self.getkeypool_opts['account'], + self.getkeypool_opts['sh_wpkh'], + self.getkeypool_opts['wpkh']) descriptors = commands.getdescriptors(self.client, 0) self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) @@ -264,6 +324,29 @@ def show_displayaddressdialog(self): self.current_dialog = DisplayAddressDialog(self.client) self.current_dialog.exec_() + @Slot() + def show_getkeypooloptionsdialog(self): + self.current_dialog = GetKeypoolOptionsDialog(self.getkeypool_opts) + self.current_dialog.accepted.connect(self.getkeypooloptionsdialog_accepted) + self.current_dialog.exec_() + + @Slot() + def getkeypooloptionsdialog_accepted(self): + self.getkeypool_opts['start'] = self.current_dialog.ui.start_spinbox.value() + self.getkeypool_opts['end'] = self.current_dialog.ui.end_spinbox.value() + self.getkeypool_opts['internal'] = self.current_dialog.ui.internal_checkbox.isChecked() + self.getkeypool_opts['keypool'] = self.current_dialog.ui.keypool_checkbox.isChecked() + self.getkeypool_opts['sh_wpkh'] = self.current_dialog.ui.sh_wpkh_radio.isChecked() + self.getkeypool_opts['wpkh'] = self.current_dialog.ui.wpkh_radio.isChecked() + if self.current_dialog.ui.account_radio.isChecked(): + self.getkeypool_opts['account'] = self.current_dialog.ui.account_spinbox.value() + self.getkeypool_opts['account_used'] = True + else: + self.getkeypool_opts['path'] = self.current_dialog.ui.path_lineedit.text() + self.getkeypool_opts['account_used'] = False + self.current_dialog = None + self.get_device_info() + def main(): app = QApplication() diff --git a/hwilib/ui/getkeypooloptionsdialog.ui b/hwilib/ui/getkeypooloptionsdialog.ui new file mode 100644 index 000000000..7ccb5bb36 --- /dev/null +++ b/hwilib/ui/getkeypooloptionsdialog.ui @@ -0,0 +1,281 @@ + + + GetKeypoolOptionsDialog + + + + 0 + 0 + 440 + 224 + + + + Dialog + + + + + 80 + 180 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 20 + 20 + 41 + 18 + + + + Start + + + + + + 20 + 60 + 31 + 18 + + + + End + + + + + + 80 + 10 + 161 + 32 + + + + 2147483647 + + + + + + 80 + 50 + 161 + 32 + + + + 2147483647 + + + 1000 + + + + + + 280 + 10 + 88 + 22 + + + + Internal + + + + + + 280 + 40 + 88 + 22 + + + + keypool + + + true + + + + + + 280 + 70 + 141 + 101 + + + + + + + + + 10 + 10 + 121 + 22 + + + + P2SH-P2WPKH + + + true + + + + + + 10 + 40 + 105 + 22 + + + + P2WPKH + + + + + + 10 + 70 + 105 + 22 + + + + P2PKH + + + + + + + 10 + 90 + 231 + 91 + + + + + + + + + 100 + 10 + 111 + 32 + + + + 2147483647 + + + 0 + + + + + + 10 + 10 + 81 + 22 + + + + Account + + + true + + + + + + 10 + 50 + 61 + 22 + + + + Path + + + + + false + + + + 80 + 50 + 141 + 32 + + + + m/0'/0'/* + + + + + + + + buttonBox + accepted() + GetKeypoolOptionsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + GetKeypoolOptionsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/hwilib/ui/hwiqt.pyproject b/hwilib/ui/hwiqt.pyproject index e210ae06c..60fccf589 100644 --- a/hwilib/ui/hwiqt.pyproject +++ b/hwilib/ui/hwiqt.pyproject @@ -1,3 +1,3 @@ { - "files": ["signmessagedialog.ui","mainwindow.ui","setpassphrasedialog.ui","getxpubdialog.ui","signpsbtdialog.ui","sendpindialog.ui","displayaddressdialog.ui"] + "files": ["signpsbtdialog.ui","mainwindow.ui","sendpindialog.ui","getxpubdialog.ui","signmessagedialog.ui","displayaddressdialog.ui","setpassphrasedialog.ui","getkeypooloptionsdialog.ui"] } From 6c9fd2db1e6338bd89e869dfb86c068667ef777d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 16:22:24 -0500 Subject: [PATCH 051/634] Use geteypool_opts for getdescriptors options --- hwilib/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index 594527404..46f485502 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -285,7 +285,7 @@ def get_device_info(self): self.getkeypool_opts['account'], self.getkeypool_opts['sh_wpkh'], self.getkeypool_opts['wpkh']) - descriptors = commands.getdescriptors(self.client, 0) + descriptors = commands.getdescriptors(self.client, self.getkeypool_opts['account']) self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) self.ui.desc_textedit.setPlainText(json.dumps(descriptors, indent=2)) From 93e8a5085f8714be8fa556c3252cdb0d89bc5a71 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Dec 2019 16:31:35 -0500 Subject: [PATCH 052/634] Enable and disable actions depending on whether a device is selected --- hwilib/gui.py | 18 ++++++++++++++++++ hwilib/ui/mainwindow.ui | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/hwilib/gui.py b/hwilib/gui.py index 46f485502..aef48fec6 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -227,6 +227,15 @@ def __init__(self): self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) + def clear_info(self): + self.ui.getxpub_button.setEnabled(False) + self.ui.signtx_button.setEnabled(False) + self.ui.signmsg_button.setEnabled(False) + self.ui.display_addr_button.setEnabled(False) + self.ui.getkeypool_opts_button.setEnabled(False) + self.ui.keypool_textedit.clear() + self.ui.desc_textedit.clear() + @Slot() def refresh_clicked(self): if self.client: @@ -244,6 +253,7 @@ def refresh_clicked(self): dev_str = '{} fingerprint:{} path:{}'.format(dev['model'], fingerprint, dev['path']) self.ui.enumerate_combobox.addItem(dev_str) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) + self.clear_info() @Slot() def show_setpassphrasedialog(self): @@ -260,8 +270,15 @@ def setpassphrasedialog_accepted(self): def get_client_and_device_info(self, index): self.ui.sendpin_button.setEnabled(False) if index == 0: + self.clear_info() return + self.ui.getxpub_button.setEnabled(True) + self.ui.signtx_button.setEnabled(True) + self.ui.signmsg_button.setEnabled(True) + self.ui.display_addr_button.setEnabled(True) + self.ui.getkeypool_opts_button.setEnabled(True) + # Get the client self.device_info = self.devices[index - 1] self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) @@ -271,6 +288,7 @@ def get_device_info(self): # Enable the sendpin button if it's a trezor and it needs it if self.device_info['needs_pin_sent']: self.ui.sendpin_button.setEnabled(True) + self.clear_info() return else: self.ui.sendpin_button.setEnabled(False) diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui index 7748bfde6..f22028f9f 100644 --- a/hwilib/ui/mainwindow.ui +++ b/hwilib/ui/mainwindow.ui @@ -107,6 +107,9 @@ + + false + Sign PSBT @@ -114,6 +117,9 @@ + + false + Get an xpub @@ -121,6 +127,9 @@ + + false + Sign Message @@ -147,6 +156,9 @@ + + false + Change the options used for getkeypool @@ -157,6 +169,9 @@ + + false + Display Address From 51296f4bdd1bc1a27576fb06884c7c40e14e2f4c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 19 Dec 2019 12:25:55 -0500 Subject: [PATCH 053/634] Show an error dialog for command errors --- hwilib/gui.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index aef48fec6..77d7cf669 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -3,6 +3,7 @@ import json from . import commands +from .errors import handle_errors try: from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog @@ -18,9 +19,19 @@ exit(-1) from PySide2.QtGui import QRegExpValidator -from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMainWindow +from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QMainWindow from PySide2.QtCore import QRegExp, Signal, Slot +def do_command(f, *args, **kwargs): + result = {} + with handle_errors(result=result): + result = f(*args, **kwargs) + if 'error' in result: + msg = 'Error: {}\nCode:{}'.format(result['error'], result['code']) + QMessageBox.critical(None, "An Error Occurred", msg) + return None + return result + class SetPassphraseDialog(QDialog): def __init__(self): super(SetPassphraseDialog, self).__init__() @@ -54,7 +65,7 @@ def __init__(self, client): self.ui.p9_button.clicked.connect(self.button_clicked(9)) self.accepted.connect(self.sendpindialog_accepted) - commands.prompt_pin(self.client) + do_command(commands.prompt_pin, self.client) def button_clicked(self, number): @Slot() @@ -67,7 +78,7 @@ def sendpindialog_accepted(self): pin = self.ui.pin_lineedit.text() # Send the pin - commands.send_pin(self.client, pin) + do_command(commands.send_pin, self.client, pin) self.client.close() self.client = None self.pin_sent_success.emit() @@ -90,7 +101,7 @@ def __init__(self, client): @Slot() def getxpub_button_clicked(self): path = self.ui.path_lineedit.text() - res = commands.getxpub(self.client, path) + res = do_command(commands.getxpub, self.client, path) self.ui.xpub_lineedit.setText(res['xpub']) class SignPSBTDialog(QDialog): @@ -109,7 +120,7 @@ def __init__(self, client): @Slot() def sign_psbt_button_clicked(self): psbt_str = self.ui.psbt_in_textedit.toPlainText() - res = commands.signtx(self.client, psbt_str) + res = do_command(commands.signtx, self.client, psbt_str) self.ui.psbt_out_textedit.setPlainText(res['psbt']) class SignMessageDialog(QDialog): @@ -130,7 +141,7 @@ def __init__(self, client): def signmsg_button_clicked(self): msg_str = self.ui.msg_textedit.toPlainText() path = self.ui.path_lineedit.text() - res = commands.signmessage(self.client, msg_str, path) + res = do_command(commands.signmessage, self.client, msg_str, path) self.ui.sig_textedit.setPlainText(res['signature']) class DisplayAddressDialog(QDialog): @@ -150,7 +161,7 @@ def __init__(self, client): @Slot() def go_button_clicked(self): path = self.ui.path_lineedit.text() - res = commands.displayaddress(self.client, path, sh_wpkh=self.ui.sh_wpkh_radio.isChecked(), wpkh=self.ui.wpkh_radio.isChecked()) + res = do_command(commands.displayaddress, self.client, path, sh_wpkh=self.ui.sh_wpkh_radio.isChecked(), wpkh=self.ui.wpkh_radio.isChecked()) self.ui.address_lineedit.setText(res['address']) class GetKeypoolOptionsDialog(QDialog): @@ -294,16 +305,16 @@ def get_device_info(self): self.ui.sendpin_button.setEnabled(False) # do getkeypool and getdescriptors - keypool = commands.getkeypool(self.client, - None if self.getkeypool_opts['account_used'] else self.getkeypool_opts['path'], - self.getkeypool_opts['start'], - self.getkeypool_opts['end'], - self.getkeypool_opts['internal'], - self.getkeypool_opts['keypool'], - self.getkeypool_opts['account'], - self.getkeypool_opts['sh_wpkh'], - self.getkeypool_opts['wpkh']) - descriptors = commands.getdescriptors(self.client, self.getkeypool_opts['account']) + keypool = do_command(commands.getkeypool, self.client, + None if self.getkeypool_opts['account_used'] else self.getkeypool_opts['path'], + self.getkeypool_opts['start'], + self.getkeypool_opts['end'], + self.getkeypool_opts['internal'], + self.getkeypool_opts['keypool'], + self.getkeypool_opts['account'], + self.getkeypool_opts['sh_wpkh'], + self.getkeypool_opts['wpkh']) + descriptors = do_command(commands.getdescriptors, self.client, self.getkeypool_opts['account']) self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) self.ui.desc_textedit.setPlainText(json.dumps(descriptors, indent=2)) From d060efc3b61a9b237fc9fb9155b09a8f19f8569a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 19 Dec 2019 15:10:41 -0500 Subject: [PATCH 054/634] Distribute hwi-qt binaries --- .travis.yml | 1 - contrib/build_bin.sh | 8 +++++--- contrib/build_wine.sh | 13 ++++++++++++- hwi-qt.spec | 43 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 ++ setup.py | 9 +++++---- 6 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 hwi-qt.spec diff --git a/.travis.yml b/.travis.yml index 7493420de..518561b06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -112,7 +112,6 @@ jobs: - name: macOS binary distribution (no tests) stage: test os: osx - osx_image: xcode7.3 language: generic addons: artifacts: diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index e7b550aa6..c486b9b32 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -11,9 +11,9 @@ poetry install # We now need to remove debugging symbols and build id from the hidapi SO file so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages -find ${so_dir} -name '*.so' -type f -execdir strip '{}' \; +strip ${so_dir}/hid*.so if [[ $OSTYPE != *"darwin"* ]]; then - find ${so_dir} -name '*.so' -type f -execdir strip -R .note.gnu.build-id '{}' \; + strip -R .note.gnu.build-id ${so_dir}/hid*.so fi # We also need to change the timestamps of all of the base library files @@ -23,6 +23,8 @@ TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" # Make the standalone binary export PYTHONHASHSEED=42 poetry run pyinstaller hwi.spec +poetry run contrib/generate-ui.sh +poetry run pyinstaller hwi-qt.spec unset PYTHONHASHSEED # Make the final compressed package @@ -32,5 +34,5 @@ OS=`uname | tr '[:upper:]' '[:lower:]'` if [[ $OS == "darwin" ]]; then OS="mac" fi -tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi +tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi hwi-qt popd diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index b441be0f8..057a27d31 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -69,13 +69,24 @@ POETRY="wine $PYHOME/Scripts/poetry.exe" sleep 5 # For some reason, pausing for a few seconds makes the next step work $POETRY install +# make the ui files +pushd hwilib/ui +for file in *.ui +do + gen_file=ui_`echo $file| cut -d. -f1`.py + $POETRY run pyside2-uic $file -o $gen_file + sed -i 's/raise()/raise_()/g' $gen_file +done +popd + # Do the build export PYTHONHASHSEED=42 $POETRY run pyinstaller hwi.spec +$POETRY run pyinstaller hwi-qt.spec unset PYTHONHASHSEED # Make the final compressed package pushd dist VERSION=`$POETRY run hwi --version | cut -d " " -f 2 | dos2unix` -zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe +zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe hwi-qt.exe popd diff --git a/hwi-qt.spec b/hwi-qt.spec new file mode 100644 index 000000000..ef34360ca --- /dev/null +++ b/hwi-qt.spec @@ -0,0 +1,43 @@ +# -*- mode: python ; coding: utf-8 -*- + +import platform + +block_cipher = None + +binaries = [] +if platform.system() == 'Windows': + binaries = [("c:/python3/libusb-1.0.dll", ".")] +elif platform.system() == 'Linux': + binaries = [("/lib/x86_64-linux-gnu/libusb-1.0.so.0", ".")] +elif platform.system() == 'Darwin': + find_brew_libusb_proc = subprocess.Popen(['brew', '--prefix', 'libusb'], stdout=subprocess.PIPE) + libusb_path = find_brew_libusb_proc.communicate()[0] + binaries = [(libusb_path.rstrip().decode() + "/lib/libusb-1.0.dylib", ".")] + +a = Analysis(['hwi-qt.py'], + binaries=binaries, + datas=[], + hiddenimports=[], + hookspath=['contrib/pyinstaller-hooks/'], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='hwi-qt', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False ) diff --git a/pyproject.toml b/pyproject.toml index d5670d33a..a989b0bdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ exclude = ["docs/", "test/"] include = ["hwilib/**/*.py", "udev/"] packages = [ { include = "hwi.py" }, + { include = "hwi-qt.py" }, { include = "hwilib" }, ] @@ -34,6 +35,7 @@ flake8 = "^3.7" [tool.poetry.scripts] hwi = 'hwilib.cli:main' +hwi-qt = 'hwilib.gui:main' [build-system] requires = ["poetry>=0.12"] diff --git a/setup.py b/setup.py index 7da501139..2ec1fcd26 100644 --- a/setup.py +++ b/setup.py @@ -11,20 +11,21 @@ 'hwilib.devices.trezorlib.transport'] package_data = \ -{'': ['*'], 'hwilib': ['udev/*']} +{'': ['*'], 'hwilib': ['udev/*', 'ui/*']} modules = \ -['hwi'] +['hwi', 'hwi-qt'] install_requires = \ ['ecdsa>=0.13.0,<0.14.0', 'hidapi>=0.7.99,<0.8.0', 'libusb1>=1.7,<2.0', 'mnemonic>=0.18.0,<0.19.0', 'pyaes>=1.6,<2.0', + 'pyside2>=5.14.0,<6.0.0', 'typing-extensions>=3.7,<4.0'] entry_points = \ -{'console_scripts': ['hwi = hwilib.cli:main']} +{'console_scripts': ['hwi = hwilib.cli:main', 'hwi-qt = hwilib.gui:main']} setup_kwargs = { 'name': 'hwi', @@ -41,7 +42,7 @@ 'py_modules': modules, 'install_requires': install_requires, 'entry_points': entry_points, - 'python_requires': '>=3.6,<4.0', + 'python_requires': '>=3.6,<3.9', } From cd24c1ab395512ae35c67858e8eb2cd20f073127 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jan 2020 16:36:03 -0500 Subject: [PATCH 055/634] Disable things for uninitialized devices --- hwilib/gui.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index 77d7cf669..fb19a4525 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -3,7 +3,7 @@ import json from . import commands -from .errors import handle_errors +from .errors import handle_errors, DEVICE_NOT_INITIALIZED try: from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog @@ -304,6 +304,12 @@ def get_device_info(self): else: self.ui.sendpin_button.setEnabled(False) + # If it isn't initialized, show an error but don't do anything + if 'code' in self.device_info and self.device_info['code'] == DEVICE_NOT_INITIALIZED: + self.clear_info() + QMessageBox.information(None, "Not initialized yet", 'Device is not initalized yet') + return + # do getkeypool and getdescriptors keypool = do_command(commands.getkeypool, self.client, None if self.getkeypool_opts['account_used'] else self.getkeypool_opts['path'], From f0d923c2c15c8ee96179d6856285860bd8c3e34b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 21 Jan 2020 12:21:57 -0500 Subject: [PATCH 056/634] Change hwi-qt main() to have command line options --- hwilib/gui.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index fb19a4525..f1846fe24 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -1,8 +1,11 @@ #! /usr/bin/env python3 import json +import logging +import sys -from . import commands +from . import commands, __version__ +from .cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED try: @@ -382,7 +385,22 @@ def getkeypooloptionsdialog_accepted(self): self.current_dialog = None self.get_device_info() -def main(): +def process_gui_commands(cli_args): + parser = HWIArgumentParser(description='Hardware Wallet Interface Qt, version {}.\nInteractively access and send commands to a hardware wallet device with a GUI. Responses are in JSON format.'.format(__version__)) + parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') + parser.add_argument('--testnet', help='Use testnet prefixes', action='store_true') + parser.add_argument('--debug', help='Print debug statements', action='store_true') + parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) + + # Parse arguments again for anything entered over stdin + args = parser.parse_args(cli_args) + + result = {} + + # Setup debug logging + logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING) + + # Qt setup app = QApplication() window = HWIQt() @@ -390,4 +408,11 @@ def main(): window.refresh_clicked() window.show() - app.exec_() + ret = app.exec_() + result = {'success': ret == 0} + + return result + +def main(): + result = process_gui_commands(sys.argv[1:]) + print(json.dumps(result)) From 0982c1e29e5e13a8acaa4afe47245009b33ad743 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 21 Jan 2020 12:27:13 -0500 Subject: [PATCH 057/634] Allow passphrase and testnet from command line --- hwilib/gui.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index f1846fe24..61d59cd69 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -207,7 +207,7 @@ def toggle_account(self, checked): self.ui.account_spinbox.setEnabled(False) class HWIQt(QMainWindow): - def __init__(self): + def __init__(self, passphrase='', testnet=False): super(HWIQt, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) @@ -216,7 +216,8 @@ def __init__(self): self.devices = [] self.client = None self.device_info = {} - self.passphrase = '' + self.passphrase = passphrase + self.testnet = testnet self.current_dialog = None self.getkeypool_opts = { 'start': 0, @@ -296,6 +297,7 @@ def get_client_and_device_info(self, index): # Get the client self.device_info = self.devices[index - 1] self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) + self.client.is_testnet = self.testnet self.get_device_info() def get_device_info(self): @@ -403,7 +405,7 @@ def process_gui_commands(cli_args): # Qt setup app = QApplication() - window = HWIQt() + window = HWIQt(args.password, args.testnet) window.refresh_clicked() From 412f62ed7aadfdc8918c854eeb46c8be604d77c4 Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Wed, 22 Jan 2020 17:41:21 +0100 Subject: [PATCH 058/634] Remove unused arg in find_device --- hwilib/cli.py | 2 +- hwilib/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index a2ead1436..dbb73ec8a 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -228,7 +228,7 @@ def process_commands(cli_args): # Auto detect if we are using fingerprint or type to identify device if args.fingerprint or (args.device_type and not args.device_path): - client = find_device(args.device_path, args.password, args.device_type, args.fingerprint) + client = find_device(args.password, args.device_type, args.fingerprint) if not client: return {'error': 'Could not find device with specified fingerprint', 'code': DEVICE_CONN_ERROR} elif args.device_type and args.device_path: diff --git a/hwilib/commands.py b/hwilib/commands.py index 9d02a9b7a..4b01855fb 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -42,7 +42,7 @@ def enumerate(password=''): return result # Fingerprint or device type required -def find_device(device_path, password='', device_type=None, fingerprint=None): +def find_device(password='', device_type=None, fingerprint=None): devices = enumerate(password) for d in devices: if device_type is not None and d['type'] != device_type and d['model'] != device_type: From 115ea00adda49368802e99feae6a441166d1db0e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jan 2020 21:16:21 -0500 Subject: [PATCH 059/634] Add --expert switch --- hwilib/cli.py | 5 +++-- hwilib/commands.py | 8 ++++---- hwilib/devices/coldcard.py | 4 ++-- hwilib/devices/digitalbitbox.py | 4 ++-- hwilib/devices/keepkey.py | 4 ++-- hwilib/devices/ledger.py | 4 ++-- hwilib/devices/trezor.py | 4 ++-- hwilib/hwwclient.py | 3 ++- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index dbb73ec8a..2d77e976f 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -107,6 +107,7 @@ def process_commands(cli_args): parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) parser.add_argument('--stdin', help='Enter commands and arguments via stdin', action='store_true') parser.add_argument('--interactive', '-i', help='Use some commands interactively. Currently required for all device configuration commands', action='store_true') + parser.add_argument('--expert', help='Do advanced things and get more detailed information returned from some commands. Use at your own risk.', action='store_true') subparsers = parser.add_subparsers(description='Commands', dest='command') # work-around to make subparser required @@ -228,12 +229,12 @@ def process_commands(cli_args): # Auto detect if we are using fingerprint or type to identify device if args.fingerprint or (args.device_type and not args.device_path): - client = find_device(args.password, args.device_type, args.fingerprint) + client = find_device(args.password, args.device_type, args.fingerprint, args.expert) if not client: return {'error': 'Could not find device with specified fingerprint', 'code': DEVICE_CONN_ERROR} elif args.device_type and args.device_path: with handle_errors(result=result, code=DEVICE_CONN_ERROR): - client = get_client(device_type, device_path, password) + client = get_client(device_type, device_path, password, args.expert) if 'error' in result: return result else: diff --git a/hwilib/commands.py b/hwilib/commands.py index 4b01855fb..6ba9e9e8e 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -12,7 +12,7 @@ from .devices import __all__ as all_devs # Get the client for the device -def get_client(device_type, device_path, password=''): +def get_client(device_type, device_path, password='', expert=False): device_type = device_type.split('_')[0] class_name = device_type.capitalize() module = device_type.lower() @@ -21,7 +21,7 @@ def get_client(device_type, device_path, password=''): try: imported_dev = importlib.import_module('.devices.' + module, __package__) client_constructor = getattr(imported_dev, class_name + 'Client') - client = client_constructor(device_path, password) + client = client_constructor(device_path, password, expert) except ImportError: if client: client.close() @@ -42,14 +42,14 @@ def enumerate(password=''): return result # Fingerprint or device type required -def find_device(password='', device_type=None, fingerprint=None): +def find_device(password='', device_type=None, fingerprint=None, expert=False): devices = enumerate(password) for d in devices: if device_type is not None and d['type'] != device_type and d['model'] != device_type: continue client = None try: - client = get_client(d['type'], d['path'], password) + client = get_client(d['type'], d['path'], password, expert) master_fpr = d.get('fingerprint', None) if master_fpr is None: diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index b27f5be4f..6240cbb91 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -36,8 +36,8 @@ def func(*args, **kwargs): # This class extends the HardwareWalletClient for ColdCard specific things class ColdcardClient(HardwareWalletClient): - def __init__(self, path, password=''): - super(ColdcardClient, self).__init__(path, password) + def __init__(self, path, password='', expert=False): + super(ColdcardClient, self).__init__(path, password, expert) # Simulator hard coded pipe socket if path == CC_SIMULATOR_SOCK: self.device = ColdcardDevice(sn=path) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 6ce05b2a7..b0da2c713 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -296,8 +296,8 @@ def format_backup_filename(name): # This class extends the HardwareWalletClient for Digital Bitbox specific things class DigitalbitboxClient(HardwareWalletClient): - def __init__(self, path, password): - super(DigitalbitboxClient, self).__init__(path, password) + def __init__(self, path, password, expert=False): + super(DigitalbitboxClient, self).__init__(path, password, expert) if not password: raise NoPasswordError('Password must be supplied for digital BitBox') if path.startswith('udp:'): diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 3c0bd900a..2d4a01b04 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -8,8 +8,8 @@ py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that class KeepkeyClient(TrezorClient): - def __init__(self, path, password=''): - super(KeepkeyClient, self).__init__(path, password) + def __init__(self, path, password='', expert=False): + super(KeepkeyClient, self).__init__(path, password, expert) self.type = 'Keepkey' def enumerate(password=''): diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index ad9e3866b..144ed75d4 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -72,8 +72,8 @@ def func(*args, **kwargs): # This class extends the HardwareWalletClient for Ledger Nano S and Nano X specific things class LedgerClient(HardwareWalletClient): - def __init__(self, path, password=''): - super(LedgerClient, self).__init__(path, password) + def __init__(self, path, password='', expert=False): + super(LedgerClient, self).__init__(path, password, expert) if path.startswith('tcp'): split_path = path.split(':') diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 444d08c61..150ceeda6 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -89,8 +89,8 @@ def interactive_get_pin(self, code=None): # This class extends the HardwareWalletClient for Trezor specific things class TrezorClient(HardwareWalletClient): - def __init__(self, path, password=''): - super(TrezorClient, self).__init__(path, password) + def __init__(self, path, password='', expert=False): + super(TrezorClient, self).__init__(path, password, expert) self.simulator = False if path.startswith('udp'): logging.debug('Simulator found, using DebugLink') diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e36e905e6..ea135b344 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -3,13 +3,14 @@ class HardwareWalletClient(object): # device is an HID device that has already been opened. - def __init__(self, path, password): + def __init__(self, path, password, expert): self.path = path self.password = password self.message_magic = b"\x18Bitcoin Signed Message:\n" self.is_testnet = False self.fingerprint = None self.xpub_cache = {} + self.expert = expert # Get the master BIP 44 pubkey def get_master_xpub(self): From 2f27388ef25f1126d50a998270baff6adedb9d9f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jan 2020 21:42:54 -0500 Subject: [PATCH 060/634] Add ExtendedKey to deserialize xpubs and xprvs --- hwilib/base58.py | 8 +++++- hwilib/serializations.py | 54 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/hwilib/base58.py b/hwilib/base58.py index 34c59cb7f..cf1c72fc0 100644 --- a/hwilib/base58.py +++ b/hwilib/base58.py @@ -8,12 +8,18 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. # -from .serializations import hash256 +import hashlib import struct from binascii import hexlify, unhexlify from typing import List b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +def sha256(s): + return hashlib.new('sha256', s).digest() + +def hash256(s): + return sha256(sha256(s)) + def encode(b: bytes) -> str: """Encode bytes to a base58-encoded string""" diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 63fc0c878..21979e341 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -18,6 +18,8 @@ from io import BytesIO, BufferedReader from codecs import encode from .errors import PSBTSerializationError +from . import base58 + import struct import binascii import hashlib @@ -833,3 +835,55 @@ def is_sane(self): if not input.is_sane(): return False return True + +# An extended public key (xpub) or private key (xprv). Just a data container for now. +# Only handles deserialization of extended keys into component data to be handled by something else +class ExtendedKey(object): + + MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' + MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' + TESTNET_PUBLIC = b'\x04\x35\x87\xCF' + TESTNET_PRIVATE = b'\x04\x35\x83\x94' + + def __init__(self): + self.is_testnet = False + self.is_private = False + self.depth = 0 + self.parent_fingerprint = b'' + self.child_num = 0 + self.chaincode = b'' + self.pubkey = b'' + self.privkey = b'' + + def deserialize(self, xpub: str): + data = base58.decode(xpub)[:-4] # Decoded xpub without checksum + + version = data[0:4] + if version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE: + self.is_testnet = True + if version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE: + self.is_private = True + + self.depth = data[4] + self.parent_fingerprint = data[5:9] + self.child_num = struct.unpack('>I', data[9:13])[0] + self.chaincode = data[13:45] + + if self.is_private: + self.privkey = data[46:] + else: + self.pubkey = data[45:78] + + def get_printable_dict(self): + d = {} + d['testnet'] = self.is_testnet + d['private'] = self.is_private + d['depth'] = self.depth + d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode() + d['child_num'] = self.child_num + d['chaincode'] = binascii.hexlify(self.chaincode).decode() + if self.is_private: + d['privkey'] = binascii.hexlify(self.privkey).decode() + else: + d['pubkey'] = binascii.hexlify(self.pubkey).decode() + return d From e2c1456b46ccecd1337415a8ca7a1c3663593904 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jan 2020 22:23:13 -0500 Subject: [PATCH 061/634] When export mode, return decoded xpub --- hwilib/devices/coldcard.py | 11 ++++++++--- hwilib/devices/digitalbitbox.py | 11 ++++++++--- hwilib/devices/ledger.py | 11 +++++++++-- hwilib/devices/trezor.py | 11 ++++++++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 6240cbb91..da7ad7bae 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -7,7 +7,7 @@ from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH from ..base58 import get_xpub_fingerprint, xpub_main_2_test -from ..serializations import PSBT +from ..serializations import ExtendedKey, PSBT from hashlib import sha256 import base64 @@ -55,9 +55,14 @@ def get_pubkey_at_path(self, path): path = path.replace('H', '\'') xpub = self.device.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) if self.is_testnet: - return {'xpub': xpub_main_2_test(xpub)} + result = {'xpub': xpub_main_2_test(xpub)} else: - return {'xpub': xpub} + result = {'xpub': xpub} + if self.expert: + xpub_obj = ExtendedKey() + xpub_obj.deserialize(xpub) + result.update(xpub_obj.get_printable_dict()) + return result def _get_fingerprint_hex(self): # quick method to get fingerprint of wallet diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index b0da2c713..92dc66d5e 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -16,7 +16,7 @@ from ..hwwclient import HardwareWalletClient from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors -from ..serializations import CTransaction, hash256, ser_sig_der, ser_sig_compact, ser_compact_size +from ..serializations import CTransaction, ExtendedKey, hash256, ser_sig_der, ser_sig_compact, ser_compact_size from ..base58 import get_xpub_fingerprint, xpub_main_2_test, get_xpub_fingerprint_hex applen = 225280 # flash size minus bootloader length @@ -321,9 +321,14 @@ def get_pubkey_at_path(self, path): raise DBBError(reply) if self.is_testnet: - return {'xpub': xpub_main_2_test(reply['xpub'])} + result = {'xpub': xpub_main_2_test(reply['xpub'])} else: - return {'xpub': reply['xpub']} + result = {'xpub': reply['xpub']} + if self.expert: + xpub_obj = ExtendedKey() + xpub_obj.deserialize(reply['xpub']) + result.update(xpub_obj.get_printable_dict()) + return result # Must return a hex string with the signed transaction # The tx must be in the PSBT format diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 144ed75d4..afe5734b5 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -12,7 +12,7 @@ import struct from .. import base58 from ..base58 import get_xpub_fingerprint_hex -from ..serializations import hash256, hash160, CTransaction +from ..serializations import ExtendedKey, hash256, hash160, CTransaction import logging import re @@ -135,7 +135,14 @@ def get_pubkey_at_path(self, path): extkey = version + depth + fpr + child + chainCode + publicKey checksum = hash256(extkey)[:4] - return {"xpub": base58.encode(extkey + checksum)} + xpub = base58.encode(extkey + checksum) + result = {"xpub": xpub} + + if self.expert: + xpub_obj = ExtendedKey() + xpub_obj.deserialize(xpub) + result.update(xpub_obj.get_printable_dict()) + return result # Must return a hex string with the signed transaction # The tx must be in the combined unsigned transaction format diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 150ceeda6..1a9ac7edb 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -10,7 +10,7 @@ from .trezorlib import tools, btc, device from .trezorlib import messages as proto from ..base58 import get_xpub_fingerprint, to_address, xpub_main_2_test, get_xpub_fingerprint_hex -from ..serializations import CTxOut, ser_uint256 +from ..serializations import CTxOut, ExtendedKey, ser_uint256 from .. import bech32 from usb1 import USBErrorNoDevice from types import MethodType @@ -126,9 +126,14 @@ def get_pubkey_at_path(self, path): raise BadArgumentError(str(e)) output = btc.get_public_node(self.client, expanded_path) if self.is_testnet: - return {'xpub': xpub_main_2_test(output.xpub)} + result = {'xpub': xpub_main_2_test(output.xpub)} else: - return {'xpub': output.xpub} + result = {'xpub': output.xpub} + if self.expert: + xpub_obj = ExtendedKey() + xpub_obj.deserialize(output.xpub) + result.update(xpub_obj.get_printable_dict()) + return result # Must return a hex string with the signed transaction # The tx must be in the psbt format From bc0521261c5a7aef1ebc47cae671cd0d6ecfae61 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 23 Jan 2020 16:03:44 -0500 Subject: [PATCH 062/634] Test --expert getxpub --- test/test_coldcard.py | 13 +++++++++++++ test/test_digitalbitbox.py | 13 +++++++++++++ test/test_keepkey.py | 11 +++++++++++ test/test_ledger.py | 19 +++++++++++++++++++ test/test_trezor.py | 11 +++++++++++ 5 files changed, 67 insertions(+) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index c3e70b473..960264d7a 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -78,9 +78,22 @@ def test_pin(self): self.assertEqual(result['error'], 'The Coldcard does not need a PIN sent from the host') self.assertEqual(result['code'], -9) + class TestColdcardGetXpub(DeviceTestCase): + def test_getxpub(self): + result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty') + self.assertTrue(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], 'bc123c3e') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '806b26507824f73bc331494afe122f428ef30dde80b2c1ce025d2d03aff411e7') + self.assertEqual(result['pubkey'], '0368000bdff5e0b71421c37b8514de8acd4d98ba9908d183d9da56d02ca4fcfd08') + # Generic device tests suite = unittest.TestSuite() suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', '', interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestColdcardGetXpub, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard_simulator', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 179ff1ab9..8eb62fff3 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -125,9 +125,22 @@ def test_backup(self): result = self.do_command(self.dev_args + ['backup', '--label', 'backup_test_backup', '--backup_passphrase', 'testpass']) self.assertTrue(result['success']) + class TestBitboxGetXpub(DeviceTestCase): + def test_getxpub(self): + result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6Du9e5Cz1NZWz3dvsvM21tsj4xEdbAb7AcbysFL42Y3yr8PLMnsaxhetHxurTpX5Rp5RbnFFwP1wct8K3gErCUSwcxFhxThsMBSxdmkhTNf') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], '31d5e5ea') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '7062818c752f878bf96ca668f77630452c3fa033b7415eed3ff568e04ada8104') + self.assertEqual(result['pubkey'], '029078c9ad8421afd958d7bc054a0952874923e2586fc9375604f0479a354ea193') + # Generic Device tests suite = unittest.TestSuite() suite.addTest(DeviceTestCase.parameterize(TestDBBManCommands, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestBitboxGetXpub, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'digitalbitbox_01_simulator', full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) diff --git a/test/test_keepkey.py b/test/test_keepkey.py index fdf00b582..d6fdb0b0d 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -137,6 +137,17 @@ def test_getxpub(self): gxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324', 'getxpub', path_vec['path']]) self.assertEqual(gxp_res['xpub'], path_vec['xpub']) + def test_expert_getxpub(self): + result = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324', '--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], 'f7e318db') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '95a7fb33c4f0896f66045cd7f45ed49a9e72372d2aed204ad0149c39b7b17905') + self.assertEqual(result['pubkey'], '022e6d9c18e5a837e802fb09abe00f787c8ccb0fc489c6ec5dc2613d930efd7eae') + # Keepkey specific management (setup, wipe, restore, backup, promptpin, sendpin) command tests class TestKeepkeyManCommands(KeepkeyTestCase): def setUp(self): diff --git a/test/test_ledger.py b/test/test_ledger.py index 0bd51e361..22e306ce9 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -186,6 +186,24 @@ def test_backup(self): self.assertEqual(result['error'], 'The Ledger Nano S and X do not support creating a backup via software') self.assertEqual(result['code'], -9) + class TestLedgerGetXpub(DeviceTestCase): + def setUp(self): + self.emulator.start() + + def tearDown(self): + self.emulator.stop() + + def test_getxpub(self): + result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6DqTtMuqBiBsSPb5UxB1qgJ3ViXuhoyZYhw3zTK4MywLB6psioW4PN1SAbhxVVirKQojnTBsjG5gXiiueRBgWmUuN43dpbMSgMCQHVqx2bR') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], '2930ce56') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], 'a3cd503ab3ffd3c31610a84307f141528c7e9b8416e10980ced60d1868b463e2') + self.assertEqual(result['pubkey'], '03d5edb7c091b5577e1e2e6493b34e602b02547518222e26472cfab1745bb5977d') + device_model = 'ledger_nano_s_simulator' path = 'tcp:127.0.0.1:9999' master_xpub = 'xpub6Cak8u8nU1evR4eMoz5UX12bU9Ws5RjEgq2Kq1RKZrsEQF6Cvecoyr19ZYRikWoJo16SXeft5fhkzbXcmuPfCzQKKB9RDPWT8XnUM62ieB9' @@ -195,6 +213,7 @@ def test_backup(self): # Generic Device tests suite = unittest.TestSuite() suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestLedgerGetXpub, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) diff --git a/test/test_trezor.py b/test/test_trezor.py index 8648a15fa..aac6e614e 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -143,6 +143,17 @@ def test_getxpub(self): gxp_res = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', 'getxpub', path_vec['path']]) self.assertEqual(gxp_res['xpub'], path_vec['xpub']) + def test_expert_getxpub(self): + result = self.do_command(['-t', 'trezor', '-d', 'udp:127.0.0.1:21324', '--expert', 'getxpub', 'm/44h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6FMafWAi3n3ET2rU5yQr16UhRD1Zx4dELmcEw3NaYeBaNnipcr2zjzYp1sNdwR3aTN37hxAqRWQ13AWUZr6L9jc617mU6EvgYXyBjXrEhgr') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], 'f7e318db') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '95a7fb33c4f0896f66045cd7f45ed49a9e72372d2aed204ad0149c39b7b17905') + self.assertEqual(result['pubkey'], '022e6d9c18e5a837e802fb09abe00f787c8ccb0fc489c6ec5dc2613d930efd7eae') + # Trezor specific management (setup, wipe, restore, backup, promptpin, sendpin) command tests class TestTrezorManCommands(TrezorTestCase): def setUp(self): From e1c1e42232d42d2d81428e0b3507318ec3a53b5e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 31 Jan 2020 18:14:39 -0500 Subject: [PATCH 063/634] Add "active" to getkeypool's output to match importdescriptors importdescriptors uses "active" instead of "keypool" to indicate that new addresses can be fetched from that import object. --- hwilib/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hwilib/commands.py b/hwilib/commands.py index 6ba9e9e8e..0363f54c7 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -104,6 +104,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import['timestamp'] = 'now' this_import['internal'] = internal this_import['keypool'] = keypool + this_import['active'] = keypool this_import['watchonly'] = True return [this_import] From f24eb9e5ab4b233ecbe2b08d5541a913becb6022 Mon Sep 17 00:00:00 2001 From: "David A. Harding" Date: Wed, 4 Mar 2020 16:23:09 -0600 Subject: [PATCH 064/634] Doc: Readme: mention calling --help after command name --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 759ceab0a..54eb4129e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,14 @@ All output will be in JSON form and sent to `stdout`. Additional information or prompts will be sent to `stderr` and will not necessarily be in JSON. This additional information is for debugging purposes. +To see a complete list of available commands and global parameters, run +`./hwi.py --help`. To see options specific to a particular command, +pass the `--help` parameter after the command name; for example: + +``` +./hwi.py getdescriptors --help +``` + ## Device Support The below table lists what devices and features are supported for each device. From 4b1c06f6d0aeb25c0b3c63786bce8fa107f61b0f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 20:13:22 -0500 Subject: [PATCH 065/634] Update keepkey build script --- test/setup_environment.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 1e889d82d..678fd69fb 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -147,18 +147,14 @@ fi # Build the simulator. This is cached, but it is also fast if [ "$keepkey_setup_needed" == true ] ; then - git clone https://github.com/nanopb/nanopb.git -b nanopb-0.2.9.2 + git clone https://github.com/nanopb/nanopb.git -b nanopb-0.3.9.4 fi -# This needs py2, so make a pipenv -export PIPENV_IGNORE_VIRTUALENVS=1 -pipenv --python 2.7 -pipenv install protobuf cd nanopb/generator/proto -pipenv run make +make cd ../../../ export PATH=$PATH:`pwd`/nanopb/generator -pipenv run cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DKK_HAVE_STRLCAT=OFF -DKK_HAVE_STRLCPY=OFF -pipenv run make -j$(nproc) kkemu +cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DPROTOC_BINARY=/usr/bin/protoc +make -j$(nproc) kkemu # Delete any emulator.img file find . -name "emulator.img" -exec rm {} \; cd .. From 535505516f77caf6d9154ef0a40d1fbfc6a8b063 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 22:03:39 -0500 Subject: [PATCH 066/634] update Trezor 1 build script --- test/setup_environment.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 678fd69fb..ed93ac190 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -36,8 +36,8 @@ fi cd legacy export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 if [ "$trezor_setup_needed" == true ] ; then - script/setup - pipenv install + pipenv sync + pipenv run script/setup fi pipenv run script/cibuild # Delete any emulator.img file From cd0720fe0ddb7768e93ac9aaa940e80710fd66aa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 22:03:51 -0500 Subject: [PATCH 067/634] tests: Use p2sh-segwit address type for the p2sh-segwit import There's a bug somewhere in Core that requires us to do this. Doing getaddressinfo on pubkeys imported with a p2sh-segwit descriptor seems to not be working if getnewaddress gives us a bech32 address. So always ask for a p2sh-segwit as a workaround. --- test/test_device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 5891b8568..d429c2135 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -205,9 +205,9 @@ def test_getkeypool(self): import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit')) self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/0/{}".format(i)) - addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/1/{}".format(i)) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20']) @@ -223,9 +223,9 @@ def test_getkeypool(self): import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit')) self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/0/{}".format(i)) - addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/1/{}".format(i)) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '--account', '3', '0', '20']) import_result = self.wrpc.importmulti(keypool_desc) From 83b60ad41c816464136f2f3db0f2a1aa352ad743 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 22:14:01 -0500 Subject: [PATCH 068/634] tests: coldcard simulator use --ms instead of -m -m for multisig was changed to --ms --- test/test_coldcard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 960264d7a..9e62eeb82 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -13,7 +13,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface): # Start the Coldcard simulator - coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator), '-m'], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) + coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator), '--ms'], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: From cb138623786c52083cd69254dbd518a0de5661e4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 22:15:42 -0500 Subject: [PATCH 069/634] tests: checkout Trezor T v2.2.0 The master branch seems to be incompatible with us right now, so use the current latest, 2.2.0, for now. --- test/setup_environment.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index ed93ac190..565d3087c 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -12,6 +12,7 @@ trezor_setup_needed=false if [ ! -d "trezor-firmware" ]; then git clone --recursive https://github.com/trezor/trezor-firmware.git cd trezor-firmware + git checkout core/v2.2.0 trezor_setup_needed=true else cd trezor-firmware From 4a4bf70fcb6ddb9a9b17e86f58569971151c7b96 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 1 Mar 2020 22:27:23 -0500 Subject: [PATCH 070/634] tests: Use new speculos cli args Speculos changed -b to --button-port and -n to --display, so we need to use those. --- test/test_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ledger.py b/test/test_ledger.py index 22e306ce9..208d86ce1 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -109,7 +109,7 @@ def __init__(self, path): def start(self): # Start the emulator - self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '-bn', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) + self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', '--button-port', '1235', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: From b8ba161df52769a34f9e183b9bc4d0e2945ec60b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 11 Mar 2020 18:51:10 -0400 Subject: [PATCH 071/634] tests: use Travis' default xcode version --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7493420de..518561b06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -112,7 +112,6 @@ jobs: - name: macOS binary distribution (no tests) stage: test os: osx - osx_image: xcode7.3 language: generic addons: artifacts: From e5808ac2766fbd3119a0cf0b1cb6871a4cafa3ba Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 18:17:43 -0400 Subject: [PATCH 072/634] tests, ledger: Have speculos do button presses internally Instead of having multiple threads and sockets to deal with sending button presses, patch speculos to do that itself. --- test/data/speculos-auto-button.patch | 64 ++++++++++++++++++ test/data/speculos-screen-text.patch | 55 ---------------- test/setup_environment.sh | 2 +- test/test_ledger.py | 98 +--------------------------- 4 files changed, 66 insertions(+), 153 deletions(-) create mode 100644 test/data/speculos-auto-button.patch delete mode 100644 test/data/speculos-screen-text.patch diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch new file mode 100644 index 000000000..3ef74e8f5 --- /dev/null +++ b/test/data/speculos-auto-button.patch @@ -0,0 +1,64 @@ +From bd251085fa85ad20e90fa055e7487b5c73d3dc7d Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Thu, 12 Mar 2020 17:25:09 -0400 +Subject: [PATCH] Do button presses within seproxyhal + +--- + mcu/seproxyhal.py | 34 ++++++++++++++++++++++++++++++++++ + 1 file changed, 34 insertions(+) + +diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py +index 8ed3fec..1a6bfb6 100644 +--- a/mcu/seproxyhal.py ++++ b/mcu/seproxyhal.py +@@ -2,6 +2,7 @@ import binascii + import logging + import os + import select ++import struct + import sys + import time + import threading +@@ -144,6 +145,39 @@ class SeProxyHal: + #self.logger.debug(f"DISPLAY_STATUS {data!r}") + screen.display_status(data) + self._queue_event_packet(SephTag.DISPLAY_PROCESSED_EVENT) ++ data_type, _, x, y, = struct.unpack('bbhh', data[:6]) ++ if data_type == 7: ++ if y == 12 or y ==28: ++ self.line = data[28:].decode() ++ elif y == 26: ++ self.line += data[28:].decode() ++ ++ if y == 26 or y == 28: ++ if self.line.startswith('Address') or self.line.startswith('Message hash') or self.line.startswith('Reviewoutput') or self.line.startswith('Amount') or self.line.startswith('Fees') or self.line.startswith('Confirmtransaction'): ++ self.handle_button(2, True) ++ self.handle_button(2, False) ++ if self.line.startswith('Message hash'): ++ self.seen_msg_hash = True ++ elif self.line == 'Approve' or self.line.startswith('Accept'): ++ self.handle_button(1, True) ++ self.handle_button(2, True) ++ self.handle_button(1, False) ++ self.handle_button(2, False) ++ elif self.line == 'Signmessage': ++ if self.seen_msg_hash: ++ self.handle_button(1, True) ++ self.handle_button(2, True) ++ self.handle_button(1, False) ++ self.handle_button(2, False) ++ self.seen_msg_hash = False ++ else: ++ self.handle_button(2, True) ++ self.handle_button(2, False) ++ self.handle_button(2, True) ++ self.handle_button(2, False) ++ elif self.line == 'Cancel' or self.line == 'Reject': ++ self.handle_button(1, True) ++ self.handle_button(1, False) + + elif tag == SephTag.SCREEN_DISPLAY_RAW_STATUS: + self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") +-- +2.25.2 + diff --git a/test/data/speculos-screen-text.patch b/test/data/speculos-screen-text.patch deleted file mode 100644 index 925f8d9b5..000000000 --- a/test/data/speculos-screen-text.patch +++ /dev/null @@ -1,55 +0,0 @@ -From 3d4ff5c0f7ffa434f73990f03b21c20153d810da Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Fri, 15 Nov 2019 18:20:25 -0500 -Subject: [PATCH] Send out screen text over unix socket - ---- - mcu/bagl.py | 13 +++++++++++++ - 1 file changed, 13 insertions(+) - -diff --git a/mcu/bagl.py b/mcu/bagl.py -index e68ab9c..8c8b3f7 100644 ---- a/mcu/bagl.py -+++ b/mcu/bagl.py -@@ -1,4 +1,8 @@ - import binascii -+import json -+import os -+import socket -+ - from collections import namedtuple - from construct import * - -@@ -61,6 +65,8 @@ SEPROXYHAL_TAG_SCREEN_DISPLAY_RAW_STATUS_START = 0x00 - - DrawState = namedtuple('DrawState', 'x y width height colors bpp xx yy') - -+SCREEN_TEXT_SOCKET = '/tmp/ledger-screen.sock' -+ - class Bagl: - def __init__(self, m, width, height): - self.m = m -@@ -69,6 +75,8 @@ class Bagl: - - self.draw_state = DrawState(0, 0, 0, 0, [], 0, 0, 0) - -+ self.screen_text_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) -+ - def refresh(self): - self.m.update() - -@@ -566,6 +574,11 @@ class Bagl: - elif type_ == BAGL_LABEL: - self._display_bagl_labeline(component, context, halignment, valignment, baseline, char_height, strwidth, type_) - elif type_ == BAGL_LABELINE: -+ try: -+ data = {"y": component.y, "text": context.decode()} -+ self.screen_text_sock.sendto(json.dumps(data).encode(), SCREEN_TEXT_SOCKET) -+ except: -+ pass - self._display_bagl_labeline(component, context, halignment, valignment, baseline, char_height, strwidth, type_) - elif type_ == BAGL_ICON: - self._display_bagl_icon(component, context) --- -2.24.0 - diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 565d3087c..f1da1e0d6 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -184,7 +184,7 @@ else fi fi # Apply patch to get screen info -git am ../../data/speculos-screen-text.patch +git am ../../data/speculos-auto-button.patch # Build the simulator. This is cached, but it is also fast mkdir -p build diff --git a/test/test_ledger.py b/test/test_ledger.py index 208d86ce1..78e1c36b7 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -1,107 +1,16 @@ #! /usr/bin/env python3 import argparse -import json import os import subprocess import signal -import socket import time import unittest from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx -from threading import Thread from hwilib.cli import process_commands -SCREEN_TEXT_SOCKET = '/tmp/ledger-screen.sock' -KEYBOARD_PORT = 1235 - - -class ScreenTextThread(Thread): - def get_screen_text(self, use_timeout=False): - if use_timeout: - self.sock.settimeout(5) - else: - self.sock.settimeout(None) - data_str = self.sock.recv(200) - if len(data_str) == 0: - return '' - data = json.loads(data_str.decode()) - - text = '' - if data['y'] == 12: # Upper line - text = data['text'] - text += self.get_screen_text() # Get next line - elif data['y'] == 26 or data['y'] == 28: # lower line or single line - text = data['text'] - return text - - def run(self): - self.running = True - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - while True: - try: - self.sock.bind(SCREEN_TEXT_SOCKET) - break - except: - os.remove(SCREEN_TEXT_SOCKET) - - self.key_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.key_sock.connect(('127.0.0.1', KEYBOARD_PORT)) - - seen_msg_hash = False - while True: - try: - text = self.get_screen_text(False) - break - except: - continue - while self.running: - just_wait = False - if text.startswith('Address') or text.startswith('Message hash') or text.startswith("Reviewoutput") or text.startswith("Amount") or text.startswith("Fees") or text == 'Confirmtransaction': - time.sleep(0.05) - self.key_sock.send(b'Rr') - if text.startswith('Message hash'): - seen_msg_hash = True - elif text == 'Approve' or text.startswith('Accept'): - time.sleep(0.05) - self.key_sock.send(b'LRlr') - elif text == 'Signmessage': - time.sleep(0.05) - if seen_msg_hash: - self.key_sock.send(b'LRlr') - seen_msg_hash = False - else: - self.key_sock.send(b'Rr') - elif text == 'Cancel' or text == 'Reject': - time.sleep(0.05) - self.key_sock.send(b'Ll') - else: - # For everything else, do nothing and wait for next text - just_wait = True - - try: - if just_wait: - # Main screen, don't do anything - new_text = self.get_screen_text(False) - else: - # Try to fetch the next text - # If it times out, maybe our input didn't make it, so try processing text again - new_text = self.get_screen_text(True) - text = new_text - except: - continue - - self.sock.close() - - def stop(self): - self.running = False - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() - self.key_sock.close() - os.remove(SCREEN_TEXT_SOCKET) - class LedgerEmulator(DeviceEmulator): def __init__(self, path): self.emulator_path = path @@ -109,7 +18,7 @@ def __init__(self, path): def start(self): # Start the emulator - self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', '--button-port', '1235', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) + self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: @@ -126,14 +35,9 @@ def start(self): pass time.sleep(0.5) - self.kp_thread = ScreenTextThread() - self.kp_thread.start() - def stop(self): - self.kp_thread.stop() os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) os.waitpid(self.emulator_proc.pid, 0) - self.kp_thread.join() def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): From a741211819657eb0352d78dd6cbff49498243394 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 22:14:59 -0400 Subject: [PATCH 073/634] tests: move setUp and tearDown to DeviceTestCase Instead of defining the same function multiple times, just do it once in the parent class. --- test/test_device.py | 20 +------------------- test/test_keepkey.py | 7 ++----- test/test_ledger.py | 12 ------------ test/test_trezor.py | 7 ++----- 4 files changed, 5 insertions(+), 41 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index d429c2135..31be6a412 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -113,13 +113,13 @@ def __str__(self): def __repr__(self): return '{}: {}'.format(self.full_type, super().__repr__()) -class TestDeviceConnect(DeviceTestCase): def setUp(self): self.emulator.start() def tearDown(self): self.emulator.stop() +class TestDeviceConnect(DeviceTestCase): def test_enumerate(self): enum_res = self.do_command(self.get_password_args() + ['enumerate']) found = False @@ -175,9 +175,6 @@ def setUp(self): self.dev_args.append('--testnet') self.emulator.start() - def tearDown(self): - self.emulator.stop() - def test_getkeypool_bad_args(self): result = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--wpkh', '0', '20']) self.assertIn('error', result) @@ -289,9 +286,6 @@ def setUp(self): self.dev_args.append('--testnet') self.emulator.start() - def tearDown(self): - self.emulator.stop() - def _generate_and_finalize(self, unknown_inputs, psbt): if not unknown_inputs: # Just do the normal signing process to test "all inputs" case @@ -464,12 +458,6 @@ def test_big_tx(self): pass class TestDisplayAddress(DeviceTestCase): - def setUp(self): - self.emulator.start() - - def tearDown(self): - self.emulator.stop() - def test_display_address_bad_args(self): result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) self.assertIn('error', result) @@ -544,12 +532,6 @@ def test_display_address_descriptor(self): self.assertEqual(result['code'], -7) class TestSignMessage(DeviceTestCase): - def setUp(self): - self.emulator.start() - - def tearDown(self): - self.emulator.stop() - def test_sign_msg(self): self.do_command(self.dev_args + ['signmessage', '"Message signing test"', 'm/44h/1h/0h/0/0']) diff --git a/test/test_keepkey.py b/test/test_keepkey.py index d6fdb0b0d..a861a926f 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -111,14 +111,14 @@ def __str__(self): def __repr__(self): return 'keepkey: {}'.format(super().__repr__()) -# Keepkey specific getxpub test because this requires device specific thing to set xprvs -class TestKeepkeyGetxpub(KeepkeyTestCase): def setUp(self): self.client = self.emulator.start() def tearDown(self): self.emulator.stop() +# Keepkey specific getxpub test because this requires device specific thing to set xprvs +class TestKeepkeyGetxpub(KeepkeyTestCase): def test_getxpub(self): 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) @@ -154,9 +154,6 @@ def setUp(self): self.client = self.emulator.start() self.dev_args = ['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324'] - def tearDown(self): - self.emulator.stop() - def test_setup_wipe(self): # Device is init, setup should fail result = self.do_command(self.dev_args + ['-i', 'setup']) diff --git a/test/test_ledger.py b/test/test_ledger.py index 78e1c36b7..0b27162f8 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -43,12 +43,6 @@ def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): # Ledger specific disabled command tests class TestLedgerDisabledCommands(DeviceTestCase): - def setUp(self): - self.emulator.start() - - def tearDown(self): - self.emulator.stop() - def test_pin(self): result = self.do_command(self.dev_args + ['promptpin']) self.assertIn('error', result) @@ -91,12 +85,6 @@ def test_backup(self): self.assertEqual(result['code'], -9) class TestLedgerGetXpub(DeviceTestCase): - def setUp(self): - self.emulator.start() - - def tearDown(self): - self.emulator.stop() - def test_getxpub(self): result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) self.assertEqual(result['xpub'], 'xpub6DqTtMuqBiBsSPb5UxB1qgJ3ViXuhoyZYhw3zTK4MywLB6psioW4PN1SAbhxVVirKQojnTBsjG5gXiiueRBgWmUuN43dpbMSgMCQHVqx2bR') diff --git a/test/test_trezor.py b/test/test_trezor.py index aac6e614e..abffc564c 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -117,14 +117,14 @@ def __str__(self): def __repr__(self): return 'trezor 1: {}'.format(super().__repr__()) -# Trezor specific getxpub test because this requires device specific thing to set xprvs -class TestTrezorGetxpub(TrezorTestCase): def setUp(self): self.client = self.emulator.start() def tearDown(self): self.emulator.stop() +# Trezor specific getxpub test because this requires device specific thing to set xprvs +class TestTrezorGetxpub(TrezorTestCase): def test_getxpub(self): 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) @@ -160,9 +160,6 @@ def setUp(self): self.client = self.emulator.start() self.dev_args = ['-t', 'trezor', '-d', 'udp:127.0.0.1:21324'] - def tearDown(self): - self.emulator.stop() - def test_setup_wipe(self): # Device is init, setup should fail result = self.do_command(self.dev_args + ['-i', 'setup']) From 3f82e60f558343ca95a772698235b60ad4dc4e23 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 22:51:55 -0400 Subject: [PATCH 074/634] test, trezor: report model T in tests correctly report the correct device name for the trezor specific tests --- test/test_trezor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_trezor.py b/test/test_trezor.py index abffc564c..f78addda0 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -112,10 +112,10 @@ def do_command(self, args): return process_commands(args) def __str__(self): - return 'trezor 1: {}'.format(super().__str__()) + return 'trezor_{}: {}'.format('t' if self.emulator.model_t else '1', super().__str__()) def __repr__(self): - return 'trezor 1: {}'.format(super().__repr__()) + return 'trezor_{}: {}'.format('t' if self.emulator.model_t else '1', super().__repr__()) def setUp(self): self.client = self.emulator.start() From 9ab7373f076fb9e2a18d9b00c26a998e11ce8ca6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 13 Mar 2020 14:03:58 -0400 Subject: [PATCH 075/634] tests: Run each device's test suite individually Instead of adding all the device tests to a single overall test suite that is run, do each device test as its own test suite with its own test runner. The change allows us to not have too many extra emulators/simulators running at once. Previously, when the tests were being loaded, if that device had a global emulator, it was started. These emulators were also left running during other device tests. By splitting up the tests per device, we can ensure that the emulators are only being run for their tests. --- test/run_tests.py | 17 +++++++++-------- test/test_coldcard.py | 10 +++++++--- test/test_digitalbitbox.py | 10 +++++++--- test/test_keepkey.py | 12 +++++++++--- test/test_ledger.py | 29 ++++++++++++++++++----------- test/test_trezor.py | 12 +++++++++--- 6 files changed, 59 insertions(+), 31 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index 9aeb17c4b..e32f18de7 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -56,6 +56,7 @@ args = parser.parse_args() # Run tests +success = True suite = unittest.TestSuite() suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress)) @@ -63,6 +64,7 @@ suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) if sys.platform.startswith("linux"): suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) +success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() if args.all: # Default all true unless overridden @@ -86,17 +88,16 @@ rpc, userpass = start_bitcoind(args.bitcoind) if args.bitbox: - suite.addTest(digitalbitbox_test_suite(args.bitbox_path, rpc, userpass, args.interface)) + success &= digitalbitbox_test_suite(args.bitbox_path, rpc, userpass, args.interface) if args.coldcard: - suite.addTest(coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface)) + success &= coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface) if args.trezor: - suite.addTest(trezor_test_suite(args.trezor_path, rpc, userpass, args.interface)) + success &= trezor_test_suite(args.trezor_path, rpc, userpass, args.interface) if args.trezor_t: - suite.addTest(trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True)) + success &= trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True) if args.keepkey: - suite.addTest(keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface)) + success &= keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface) if args.ledger: - suite.addTest(ledger_test_suite(args.ledger_path, rpc, userpass, args.interface)) + success &= ledger_test_suite(args.ledger_path, rpc, userpass, args.interface) -result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) -sys.exit(not result.wasSuccessful()) +sys.exit(not success) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 9e62eeb82..f246df4eb 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -5,6 +5,7 @@ import os import signal import subprocess +import sys import time import unittest @@ -101,7 +102,11 @@ def test_getxpub(self): suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface)) - return suite + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + cleanup_simulator() + atexit.unregister(cleanup_simulator) + return result.wasSuccessful() if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Coldcard implementation') @@ -113,5 +118,4 @@ def test_getxpub(self): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = coldcard_test_suite(args.simulator, rpc, userpass, args.interface) - unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not coldcard_test_suite(args.simulator, rpc, userpass, args.interface)) diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 8eb62fff3..28945ef74 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -5,6 +5,7 @@ import json import os import subprocess +import sys import time import unittest @@ -147,7 +148,11 @@ def test_getxpub(self): suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) - return suite + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + cleanup_simulator() + atexit.unregister(cleanup_simulator) + return result.wasSuccessful() if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Digital Bitbox implementation') @@ -159,5 +164,4 @@ def test_getxpub(self): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = digitalbitbox_test_suite(args.simulator, rpc, userpass, args.interface) - unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not digitalbitbox_test_suite(args.simulator, rpc, userpass, args.interface)) diff --git a/test/test_keepkey.py b/test/test_keepkey.py index a861a926f..09b8390b1 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -1,6 +1,7 @@ #! /usr/bin/env python3 import argparse +import atexit import json import os import shlex @@ -59,6 +60,7 @@ def start(self): client.init_device() device.wipe(client) load_device_by_mnemonic(client=client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=False, label='test') # From Trezor device tests + atexit.register(self.stop) return client def stop(self): @@ -70,6 +72,8 @@ def stop(self): if os.path.isfile(emulator_img): os.unlink(emulator_img) + atexit.unregister(self.stop) + class KeepkeyTestCase(unittest.TestCase): def __init__(self, emulator, interface='library', methodName='runTest'): super(KeepkeyTestCase, self).__init__(methodName) @@ -312,7 +316,10 @@ def keepkey_test_suite(emulator, rpc, userpass, interface): suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyGetxpub, emulator=dev_emulator, interface=interface)) suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyManCommands, emulator=dev_emulator, interface=interface)) - return suite + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + sys.stderr = sys.__stderr__ + return result.wasSuccessful() if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Keepkey implementation') @@ -324,5 +331,4 @@ def keepkey_test_suite(emulator, rpc, userpass, interface): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = keepkey_test_suite(args.emulator, rpc, userpass, args.interface) - unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + sys.exit(not keepkey_test_suite(args.emulator, rpc, userpass, args.interface)) diff --git a/test/test_ledger.py b/test/test_ledger.py index 0b27162f8..d636d0e56 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -1,9 +1,11 @@ #! /usr/bin/env python3 import argparse +import atexit import os import subprocess import signal +import sys import time import unittest @@ -101,19 +103,25 @@ def test_getxpub(self): master_xpub = 'xpub6Cak8u8nU1evR4eMoz5UX12bU9Ws5RjEgq2Kq1RKZrsEQF6Cvecoyr19ZYRikWoJo16SXeft5fhkzbXcmuPfCzQKKB9RDPWT8XnUM62ieB9' fingerprint = 'f5acc2fd' dev_emulator = LedgerEmulator(emulator) + dev_emulator.start() + atexit.register(dev_emulator.stop) # Generic Device tests suite = unittest.TestSuite() - suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestLedgerGetXpub, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestLedgerGetXpub, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) if signtx: - suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - return suite + suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + dev_emulator.stop() + atexit.unregister(dev_emulator.stop) + return result.wasSuccessful() if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Ledger implementation') @@ -127,5 +135,4 @@ def test_getxpub(self): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = ledger_test_suite(args.emulator, rpc, userpass, args.interface, args.signtx) - unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not ledger_test_suite(args.emulator, rpc, userpass, args.interface, args.signtx)) diff --git a/test/test_trezor.py b/test/test_trezor.py index f78addda0..cb6ddaaa9 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -1,6 +1,7 @@ #! /usr/bin/env python3 import argparse +import atexit import json import os import shlex @@ -61,6 +62,7 @@ def start(self): client.init_device() device.wipe(client) load_device_by_mnemonic(client=client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=False, label='test') # From Trezor device tests + atexit.register(self.stop) return client def stop(self): @@ -76,6 +78,8 @@ def stop(self): if os.path.isfile(emulator_img): os.unlink(emulator_img) + atexit.unregister(self.stop) + class TrezorTestCase(unittest.TestCase): def __init__(self, emulator, interface='library', methodName='runTest'): super(TrezorTestCase, self).__init__(methodName) @@ -333,7 +337,10 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface)) else: suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_t_simulator', full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) - return suite + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + sys.stderr = sys.__stderr__ + return result.wasSuccessful() if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test Trezor implementation') @@ -346,5 +353,4 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - suite = trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model_t) - unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + sys.exit(not trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model_t)) From 9b31a96f135381d10cc215173e9b6f17ad92035a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 18 Mar 2020 18:19:53 -0400 Subject: [PATCH 076/634] tests: Only send kill signal if process isn't already dead --- test/test_coldcard.py | 5 +++-- test/test_ledger.py | 5 +++-- test/test_trezor.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index f246df4eb..ea27c02c5 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -32,8 +32,9 @@ def coldcard_test_suite(simulator, rpc, userpass, interface): # Cleanup def cleanup_simulator(): - os.killpg(os.getpgid(coldcard_proc.pid), signal.SIGTERM) - os.waitpid(os.getpgid(coldcard_proc.pid), 0) + if coldcard_proc.poll() is None: + os.killpg(os.getpgid(coldcard_proc.pid), signal.SIGTERM) + os.waitpid(os.getpgid(coldcard_proc.pid), 0) atexit.register(cleanup_simulator) # Coldcard specific management command tests diff --git a/test/test_ledger.py b/test/test_ledger.py index d636d0e56..ebb2db080 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -38,8 +38,9 @@ def start(self): time.sleep(0.5) def stop(self): - os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) - os.waitpid(self.emulator_proc.pid, 0) + if self.emulator_proc.poll() is None: + os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) + os.waitpid(self.emulator_proc.pid, 0) def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): diff --git a/test/test_trezor.py b/test/test_trezor.py index cb6ddaaa9..7237f08b7 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -66,8 +66,9 @@ def start(self): return client def stop(self): - os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) - os.waitpid(self.emulator_proc.pid, 0) + if self.emulator_proc.poll() is None: + os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) + os.waitpid(self.emulator_proc.pid, 0) # Clean up emulator image if self.model_t: From 4a8f88e48381b4c2ab341fa1eb931c12f40e3ee6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 19 Mar 2020 18:34:14 -0400 Subject: [PATCH 077/634] tests: Non-device tests do not need emulators compiled --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 518561b06..b872f5346 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,6 +69,9 @@ jobs: script: mypy hwilib/base58.py - name: Run non-device tests only stage: test + install: + - pip install poetry + - poetry install script: cd test; poetry run ./run_tests.py - name: With process_commands interface stage: test From f719449b4e918bf62865cc5207d81ff68b7c2726 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Mar 2020 12:13:51 -0400 Subject: [PATCH 078/634] tests: Add missing self.seen_msg_hash to speculos patch --- test/data/speculos-auto-button.patch | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch index 3ef74e8f5..81488f143 100644 --- a/test/data/speculos-auto-button.patch +++ b/test/data/speculos-auto-button.patch @@ -1,14 +1,14 @@ -From bd251085fa85ad20e90fa055e7487b5c73d3dc7d Mon Sep 17 00:00:00 2001 +From 5622600c3cd28b4eb1fc7b6bb29e5b906674339e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 17:25:09 -0400 Subject: [PATCH] Do button presses within seproxyhal --- - mcu/seproxyhal.py | 34 ++++++++++++++++++++++++++++++++++ - 1 file changed, 34 insertions(+) + mcu/seproxyhal.py | 35 +++++++++++++++++++++++++++++++++++ + 1 file changed, 35 insertions(+) diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py -index 8ed3fec..1a6bfb6 100644 +index 8ed3fec..77d3ede 100644 --- a/mcu/seproxyhal.py +++ b/mcu/seproxyhal.py @@ -2,6 +2,7 @@ import binascii @@ -19,7 +19,15 @@ index 8ed3fec..1a6bfb6 100644 import sys import time import threading -@@ -144,6 +145,39 @@ class SeProxyHal: +@@ -58,6 +59,7 @@ class SeProxyHal: + self.status_received = True + self.usb = usb.USB(self._queue_event_packet) + self.logger = logging.getLogger("seproxyhal") ++ self.seen_msg_hash = False + + def _recvall(self, size): + data = b'' +@@ -144,6 +146,39 @@ class SeProxyHal: #self.logger.debug(f"DISPLAY_STATUS {data!r}") screen.display_status(data) self._queue_event_packet(SephTag.DISPLAY_PROCESSED_EVENT) From 0373bf9856ff1788235fecea3e228b87829851bd Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Mar 2020 16:08:17 -0400 Subject: [PATCH 079/634] tests: exit early if previous device fails --- test/run_tests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index e32f18de7..e3969dea8 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -87,17 +87,17 @@ # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - if args.bitbox: + if success and args.bitbox: success &= digitalbitbox_test_suite(args.bitbox_path, rpc, userpass, args.interface) - if args.coldcard: + if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface) - if args.trezor: + if success and args.trezor: success &= trezor_test_suite(args.trezor_path, rpc, userpass, args.interface) - if args.trezor_t: + if success and args.trezor_t: success &= trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True) - if args.keepkey: + if success and args.keepkey: success &= keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface) - if args.ledger: + if success and args.ledger: success &= ledger_test_suite(args.ledger_path, rpc, userpass, args.interface) sys.exit(not success) From 93778ebd2ce904a2f8cf13d5e16dc38f8e6acac0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Mar 2020 16:30:17 -0400 Subject: [PATCH 080/634] tests: redirect emulator output to logfiles --- .gitignore | 3 +++ test/test_coldcard.py | 8 +++++++- test/test_digitalbitbox.py | 8 +++++++- test/test_keepkey.py | 11 ++++++++++- test/test_ledger.py | 13 ++++++++++++- test/test_trezor.py | 12 +++++++++++- 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 8a4822368..5d534bb44 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ pip-wheel-metadata # Qt stuff hwiqt.pyproject.user hwilib/ui/ui_*.py + +*.stderr +*.stdout diff --git a/test/test_coldcard.py b/test/test_coldcard.py index ea27c02c5..572041c80 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -13,8 +13,13 @@ from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx def coldcard_test_suite(simulator, rpc, userpass, interface): + try: + os.unlink('coldcard-emulator.stdout') + except FileNotFoundError: + pass + coldcard_log = open('coldcard-emulator.stdout', 'a') # Start the Coldcard simulator - coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator), '--ms'], cwd=os.path.dirname(simulator), stdout=subprocess.DEVNULL, preexec_fn=os.setsid) + coldcard_proc = subprocess.Popen(['python3', os.path.basename(simulator), '--ms'], cwd=os.path.dirname(simulator), stdout=coldcard_log, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: @@ -35,6 +40,7 @@ def cleanup_simulator(): if coldcard_proc.poll() is None: os.killpg(os.getpgid(coldcard_proc.pid), signal.SIGTERM) os.waitpid(os.getpgid(coldcard_proc.pid), 0) + coldcard_log.close() atexit.register(cleanup_simulator) # Coldcard specific management command tests diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 28945ef74..9d196ee9f 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -14,8 +14,13 @@ from hwilib.devices.digitalbitbox import BitboxSimulator, send_plain, send_encrypt def digitalbitbox_test_suite(simulator, rpc, userpass, interface): + try: + os.unlink('bitbox-emulator.stderr') + except FileNotFoundError: + pass + bitbox_log = open('bitbox-emulator.stderr', 'a') # Start the Digital bitbox simulator - simulator_proc = subprocess.Popen(['./' + os.path.basename(simulator), '../../tests/sd_files/'], cwd=os.path.dirname(simulator), stderr=subprocess.DEVNULL) + simulator_proc = subprocess.Popen(['./' + os.path.basename(simulator), '../../tests/sd_files/'], cwd=os.path.dirname(simulator), stderr=bitbox_log) # Wait for simulator to be up while True: try: @@ -31,6 +36,7 @@ def digitalbitbox_test_suite(simulator, rpc, userpass, interface): def cleanup_simulator(): simulator_proc.terminate() simulator_proc.wait() + bitbox_log.close() atexit.register(cleanup_simulator) # Set password and load from backup diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 09b8390b1..1ae7e24a4 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -32,10 +32,16 @@ class KeepkeyEmulator(DeviceEmulator): def __init__(self, path): self.emulator_path = path self.emulator_proc = None + self.keepkey_log = None + try: + os.unlink('keepkey-emulator.stdout') + except FileNotFoundError: + pass def start(self): + self.keepkey_log = open('keepkey-emulator.stdout', 'a') # Start the Keepkey emulator - self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL) + self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.keepkey_log) # 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) @@ -72,6 +78,9 @@ def stop(self): if os.path.isfile(emulator_img): os.unlink(emulator_img) + if self.keepkey_log is not None: + self.keepkey_log.close() + atexit.unregister(self.stop) class KeepkeyTestCase(unittest.TestCase): diff --git a/test/test_ledger.py b/test/test_ledger.py index ebb2db080..ba63fab9c 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -17,10 +17,17 @@ class LedgerEmulator(DeviceEmulator): def __init__(self, path): self.emulator_path = path self.emulator_proc = None + self.emulator_stderr = None + self.emulator_stdout = None + try: + os.unlink('ledger-emulator.stderr') + except FileNotFoundError: + pass def start(self): + self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator - self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) + self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stderr=self.emulator_stderr, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: @@ -41,6 +48,10 @@ def stop(self): if self.emulator_proc.poll() is None: os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGTERM) os.waitpid(self.emulator_proc.pid, 0) + if self.emulator_stderr is not None: + self.emulator_stderr.close() + if self.emulator_stdout is not None: + self.emulator_stdout.close() def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): diff --git a/test/test_trezor.py b/test/test_trezor.py index 7237f08b7..89e325b3f 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -34,10 +34,16 @@ def __init__(self, path, model_t): self.emulator_path = path self.emulator_proc = None self.model_t = model_t + self.emulator_log = None + try: + os.unlink('trezor-{}-emulator.stdout'.format('t' if model_t else '1')) + except FileNotFoundError: + pass def start(self): + self.emulator_log = open('trezor-{}-emulator.stdout'.format('t' if self.model_t else '1'), 'a') # Start the Trezor emulator - self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid) + self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.emulator_log, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid) # 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) @@ -79,6 +85,10 @@ def stop(self): if os.path.isfile(emulator_img): os.unlink(emulator_img) + if self.emulator_log is not None: + self.emulator_log.close() + self.emulator_log = None + atexit.unregister(self.stop) class TrezorTestCase(unittest.TestCase): From 2ce7a5e0097c4cd53ea4a8f74b439f8fa80b5fc2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Mar 2020 18:26:00 -0400 Subject: [PATCH 081/634] tests: Print emulator logs on failure --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b872f5346..9dbbfc5e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,6 +55,8 @@ install: - 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 - poetry install +after_failure: + - tail -v -n +1 *.std* jobs: include: - name: lint From 124918cff86e2ffb6d05c61bb41d81b5a75b274a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 23 Mar 2020 12:27:10 -0400 Subject: [PATCH 082/634] tests: Update the speculos patch --- test/data/speculos-auto-button.patch | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch index 81488f143..db9741d8a 100644 --- a/test/data/speculos-auto-button.patch +++ b/test/data/speculos-auto-button.patch @@ -1,4 +1,4 @@ -From 5622600c3cd28b4eb1fc7b6bb29e5b906674339e Mon Sep 17 00:00:00 2001 +From b3156bb4705a55dd03fe40465bd5574c472ba335 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 17:25:09 -0400 Subject: [PATCH] Do button presses within seproxyhal @@ -8,7 +8,7 @@ Subject: [PATCH] Do button presses within seproxyhal 1 file changed, 35 insertions(+) diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py -index 8ed3fec..77d3ede 100644 +index fffba52..462456a 100644 --- a/mcu/seproxyhal.py +++ b/mcu/seproxyhal.py @@ -2,6 +2,7 @@ import binascii @@ -19,18 +19,18 @@ index 8ed3fec..77d3ede 100644 import sys import time import threading -@@ -58,6 +59,7 @@ class SeProxyHal: - self.status_received = True - self.usb = usb.USB(self._queue_event_packet) - self.logger = logging.getLogger("seproxyhal") +@@ -129,6 +130,7 @@ class SeProxyHal: + daemon=True) + self.ticker_thread.start() + self.usb = usb.USB(self.packet_thread.queue_packet) + self.seen_msg_hash = False def _recvall(self, size): data = b'' -@@ -144,6 +146,39 @@ class SeProxyHal: +@@ -168,6 +170,39 @@ class SeProxyHal: #self.logger.debug(f"DISPLAY_STATUS {data!r}") screen.display_status(data) - self._queue_event_packet(SephTag.DISPLAY_PROCESSED_EVENT) + self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) + data_type, _, x, y, = struct.unpack('bbhh', data[:6]) + if data_type == 7: + if y == 12 or y ==28: From ab86822ea9139c0e8d9a5ede103d1047e47c7b8c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 25 Mar 2020 14:52:50 -0400 Subject: [PATCH 083/634] tests, ledger: enable signtx tests for ledger --- test/test_ledger.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/test_ledger.py b/test/test_ledger.py index ba63fab9c..0c5f3612e 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -53,7 +53,7 @@ def stop(self): if self.emulator_stdout is not None: self.emulator_stdout.close() -def ledger_test_suite(emulator, rpc, userpass, interface, signtx=False): +def ledger_test_suite(emulator, rpc, userpass, interface): # Ledger specific disabled command tests class TestLedgerDisabledCommands(DeviceTestCase): @@ -127,8 +127,7 @@ def test_getxpub(self): suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) - if signtx: - suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, device_model, 'ledger', path, fingerprint, master_xpub, interface=interface)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) dev_emulator.stop() @@ -140,11 +139,10 @@ def test_getxpub(self): parser.add_argument('emulator', help='Path to the ledger emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') - parser.add_argument('--signtx', help='Run the transaction signing tests too', action='store_true') args = parser.parse_args() # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - sys.exit(not ledger_test_suite(args.emulator, rpc, userpass, args.interface, args.signtx)) + sys.exit(not ledger_test_suite(args.emulator, rpc, userpass, args.interface)) From 06b5bf07a46b5a014c7507e41a7a9a9ea1d31d8d Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Thu, 23 Jan 2020 11:37:24 +0100 Subject: [PATCH 084/634] Add get fingerprint function to HardwareWalletClient class As pre-work for the bitbox02 integration, add a function to the device class to get the master fingerprint. It can later be overloaded by the bitbox02 definitions. The existing hardware wallet clients now use this function to get the fingerprint. Coldcard's existing fingerprint function is renamed to overload the function definition. Also removes the `get_xpub_fingerprint_as_id` function, since it does the same as `get_xpub_fingerprint_hex`. --- hwilib/base58.py | 5 ----- hwilib/commands.py | 30 +++++++++++------------------- hwilib/devices/coldcard.py | 4 ++-- hwilib/devices/digitalbitbox.py | 5 ++--- hwilib/devices/keepkey.py | 4 +--- hwilib/devices/ledger.py | 4 +--- hwilib/devices/trezor.py | 5 ++--- hwilib/hwwclient.py | 7 +++++++ 8 files changed, 26 insertions(+), 38 deletions(-) diff --git a/hwilib/base58.py b/hwilib/base58.py index cf1c72fc0..27bddafb3 100644 --- a/hwilib/base58.py +++ b/hwilib/base58.py @@ -82,11 +82,6 @@ def get_xpub_fingerprint_hex(xpub: str) -> str: fingerprint = data[5:9] return hexlify(fingerprint).decode() -def get_xpub_fingerprint_as_id(xpub: str) -> str: - data = decode(xpub) - fingerprint = data[5:9] - return hexlify(fingerprint).decode() - def to_address(b: bytes, version: bytes) -> str: data = version + b checksum = hash256(data)[0:4] diff --git a/hwilib/commands.py b/hwilib/commands.py index 6ba9e9e8e..330649827 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -6,7 +6,7 @@ import platform from .serializations import PSBT -from .base58 import get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex, xpub_to_pub_hex +from .base58 import xpub_to_pub_hex from .errors import UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED from .descriptor import Descriptor from .devices import __all__ as all_devs @@ -53,15 +53,12 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False): master_fpr = d.get('fingerprint', None) if master_fpr is None: - master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] - master_fpr = get_xpub_fingerprint_hex(master_xpub) + master_fpr = client.get_master_fingerprint_hex() if fingerprint and master_fpr != fingerprint: client.close() continue - else: - client.fingerprint = master_fpr - return client + return client except: if client: client.close() @@ -88,11 +85,11 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc return {'error': 'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.', 'code': BAD_ARGUMENT} try: - master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] + master_fpr = client.get_master_fingerprint_hex() except NotImplementedError as e: return {'error': str(e), 'code': NOT_IMPLEMENTED} - desc = getdescriptor(client, master_xpub, client.is_testnet, path, internal, sh_wpkh, wpkh, account, start, end) + desc = getdescriptor(client, master_fpr, client.is_testnet, path, internal, sh_wpkh, wpkh, account, start, end) if not isinstance(desc, Descriptor): return desc @@ -107,8 +104,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import['watchonly'] = True return [this_import] -def getdescriptor(client, master_xpub, testnet=False, path=None, internal=False, sh_wpkh=False, wpkh=True, account=0, start=None, end=None): - master_fpr = get_xpub_fingerprint_as_id(master_xpub) +def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, sh_wpkh=False, wpkh=True, account=0, start=None, end=None): testnet = client.is_testnet if not path: @@ -175,7 +171,7 @@ def getkeypool(client, path, start, end, internal=False, keypool=True, account=0 def getdescriptors(client, account=0): try: - master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] + master_fpr = client.get_master_fingerprint_hex() except NotImplementedError as e: return {'error': str(e), 'code': NOT_IMPLEMENTED} @@ -183,9 +179,9 @@ def getdescriptors(client, account=0): for internal in [False, True]: descriptors = [] - desc1 = getdescriptor(client, master_xpub=master_xpub, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=False, account=account) - desc2 = getdescriptor(client, master_xpub=master_xpub, testnet=client.is_testnet, internal=internal, sh_wpkh=True, wpkh=False, account=account) - desc3 = getdescriptor(client, master_xpub=master_xpub, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=True, account=account) + desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=False, account=account) + desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=True, wpkh=False, account=account) + desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=True, account=account) for desc in [desc1, desc2, desc3]: if not isinstance(desc, Descriptor): return desc @@ -203,10 +199,6 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False): return {'error': 'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.', 'code': BAD_ARGUMENT} return client.display_address(path, sh_wpkh, wpkh) elif desc is not None: - if client.fingerprint is None: - master_xpub = client.get_pubkey_at_path('m/0h')['xpub'] - client.fingerprint = get_xpub_fingerprint_hex(master_xpub) - if sh_wpkh or wpkh: return {'error': ' `--wpkh` and `--sh_wpkh` can not be combined with --desc', 'code': BAD_ARGUMENT} descriptor = Descriptor.parse(desc, client.is_testnet) @@ -214,7 +206,7 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False): return {'error': 'Unable to parse descriptor: ' + desc, 'code': BAD_ARGUMENT} if descriptor.m_path is None: return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} - if descriptor.origin_fingerprint != client.fingerprint: + if descriptor.origin_fingerprint != client.get_master_fingerprint_hex(): return {'error': 'Descriptor fingerprint does not match device: ' + desc, 'code': BAD_ARGUMENT} xpub = client.get_pubkey_at_path(descriptor.m_path_base)['xpub'] if descriptor.base_key != xpub and descriptor.base_key != xpub_to_pub_hex(xpub): diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index da7ad7bae..913578604 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -64,7 +64,7 @@ def get_pubkey_at_path(self, path): result.update(xpub_obj.get_printable_dict()) return result - def _get_fingerprint_hex(self): + def get_master_fingerprint_hex(self): # quick method to get fingerprint of wallet return hexlify(struct.pack(' Date: Tue, 7 Apr 2020 17:01:26 +0300 Subject: [PATCH 085/634] Support setting word count for `restore_device` Support setting word count when calling client me `restore_device` and with CLI `restore` command. --- hwilib/cli.py | 3 ++- hwilib/commands.py | 4 ++-- hwilib/devices/coldcard.py | 2 +- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/ledger.py | 2 +- hwilib/devices/trezor.py | 4 ++-- hwilib/hwwclient.py | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 2d77e976f..123f762ee 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -42,7 +42,7 @@ def getdescriptors_handler(args, client): def restore_device_handler(args, client): if args.interactive: - return restore_device(client, label=args.label) + return restore_device(client, label=args.label, word_count=args.word_count) return {'error': 'restore requires interactive mode', 'code': UNAVAILABLE_ACTION} def setup_device_handler(args, client): @@ -166,6 +166,7 @@ def process_commands(cli_args): wipedev_parser.set_defaults(func=wipe_device_handler) restore_parser = subparsers.add_parser('restore', help='Initiate the device restoring process. Requires interactive mode') + restore_parser.add_argument('--word_count', '-w', help='Word count of your BIP39 recovery phrase (options: 12/18/24)', type=int, default=24) restore_parser.add_argument('--label', '-l', help='The name to give to the device', default='') restore_parser.set_defaults(func=restore_device_handler) diff --git a/hwilib/commands.py b/hwilib/commands.py index 0363f54c7..4b1995905 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -228,8 +228,8 @@ def setup_device(client, label='', backup_passphrase=''): def wipe_device(client): return client.wipe_device() -def restore_device(client, label): - return client.restore_device(label) +def restore_device(client, label='', word_count=24): + return client.restore_device(label, word_count) def backup_device(client, label='', backup_passphrase=''): return client.backup_device(label, backup_passphrase) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index da7ad7bae..6ccbcc203 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -197,7 +197,7 @@ def wipe_device(self): raise UnavailableActionError('The Coldcard does not support wiping via software') # Restore device from mnemonic or xprv - def restore_device(self, label=''): + def restore_device(self, label='', word_count=24): raise UnavailableActionError('The Coldcard does not support restoring via software') # Begin backup process diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 92dc66d5e..e2c6d167f 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -553,7 +553,7 @@ def wipe_device(self): return {'success': True} # Restore device from mnemonic or xprv - def restore_device(self, label=''): + def restore_device(self, label='', word_count=24): raise UnavailableActionError('The Digital Bitbox does not support restoring via software') # Begin backup process diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index afe5734b5..078d3f29b 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -335,7 +335,7 @@ def wipe_device(self): raise UnavailableActionError('The Ledger Nano S and X do not support wiping via software') # Restore device from mnemonic or xprv - def restore_device(self, label=''): + def restore_device(self, label='', word_count=24): raise UnavailableActionError('The Ledger Nano S and X do not support restoring via software') # Begin backup process diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 1a9ac7edb..8e5411e7d 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -385,13 +385,13 @@ def wipe_device(self): # Restore device from mnemonic or xprv @trezor_exception - def restore_device(self, label=''): + def restore_device(self, label='', word_count=24): self.client.init_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) - device.recover(self.client, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password)) + device.recover(self.client, word_count=word_count, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password)) return {'success': True} # Begin backup process diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index ea135b344..dbbdb232f 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -45,7 +45,7 @@ def wipe_device(self): 'implement this method') # Restore device from mnemonic or xprv - def restore_device(self, label=''): + def restore_device(self, label='', word_count=24): raise NotImplementedError('The HardwareWalletClient base class does not implement this method') # Begin backup process From c8d9db693b721f0ff85fdd05ee0de80e950f0954 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 15 Apr 2020 11:33:26 +0300 Subject: [PATCH 086/634] Support Trezor and KeepKey toggle passphrase Add `togglepassphrase` command which toggles the passphrase protection functionality for Trezor and KeepKey devices. --- docs/keepkey.md | 1 + docs/trezor.md | 1 + hwilib/cli.py | 8 +++++++- hwilib/commands.py | 3 +++ hwilib/devices/coldcard.py | 4 ++++ hwilib/devices/digitalbitbox.py | 4 ++++ hwilib/devices/ledger.py | 4 ++++ hwilib/devices/trezor.py | 6 ++++++ hwilib/hwwclient.py | 4 ++++ test/test_keepkey.py | 36 ++++++++++++++++----------------- test/test_trezor.py | 35 ++++++++++++++++---------------- 11 files changed, 70 insertions(+), 36 deletions(-) diff --git a/docs/keepkey.md b/docs/keepkey.md index 3cb5d8221..7b1d12f42 100644 --- a/docs/keepkey.md +++ b/docs/keepkey.md @@ -13,6 +13,7 @@ Current implemented commands are: - `signtx` - `displayaddress` - `signmessage` +- `togglepassphrase` ## `signtx` Caveats diff --git a/docs/trezor.md b/docs/trezor.md index f8ff06df1..64a8f2985 100644 --- a/docs/trezor.md +++ b/docs/trezor.md @@ -12,6 +12,7 @@ Current implemented commands are: - `wipe` - `restore` - `backup` +- `togglepassphrase` ## `signtx` Caveats diff --git a/hwilib/cli.py b/hwilib/cli.py index 2d77e976f..7ac77141a 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -1,7 +1,7 @@ #! /usr/bin/env python3 from .commands import backup_device, displayaddress, enumerate, find_device, \ - get_client, getmasterxpub, getxpub, getkeypool, getdescriptors, prompt_pin, restore_device, send_pin, setup_device, \ + get_client, getmasterxpub, getxpub, getkeypool, getdescriptors, prompt_pin, toggle_passphrase, restore_device, send_pin, setup_device, \ signmessage, signtx, wipe_device, install_udev_rules from .errors import ( handle_errors, @@ -62,6 +62,9 @@ def wipe_device_handler(args, client): def prompt_pin_handler(args, client): return prompt_pin(client) +def toggle_passphrase_handler(args, client): + return toggle_passphrase(client) + def send_pin_handler(args, client): return send_pin(client, pin=args.pin) @@ -177,6 +180,9 @@ def process_commands(cli_args): promptpin_parser = subparsers.add_parser('promptpin', help='Have the device prompt for your PIN') promptpin_parser.set_defaults(func=prompt_pin_handler) + togglepassphrase_parser = subparsers.add_parser('togglepassphrase', help='Toggle BIP39 passphrase protection') + togglepassphrase_parser.set_defaults(func=toggle_passphrase_handler) + sendpin_parser = subparsers.add_parser('sendpin', help='Send the numeric positions for your PIN to the device') sendpin_parser.add_argument('pin', help='The numeric positions of the PIN') sendpin_parser.set_defaults(func=send_pin_handler) diff --git a/hwilib/commands.py b/hwilib/commands.py index 0363f54c7..3641ebe02 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -240,6 +240,9 @@ def prompt_pin(client): def send_pin(client, pin): return client.send_pin(pin) +def toggle_passphrase(client): + return client.toggle_passphrase() + def install_udev_rules(source, location): if platform.system() == "Linux": from .udevinstaller import UDevInstaller diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index da7ad7bae..54ee636b9 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -242,6 +242,10 @@ def prompt_pin(self): def send_pin(self, pin): raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') + # Toggle passphrase + def toggle_passphrase(self): + raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host') + def enumerate(password=''): results = [] devices = hid.enumerate(COINKITE_VID, CKCC_PID) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 92dc66d5e..18b1bf93a 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -583,6 +583,10 @@ def prompt_pin(self): def send_pin(self, pin): raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') + # Toggle passphrase + def toggle_passphrase(self): + raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host') + def enumerate(password=''): results = [] devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index afe5734b5..798df4d07 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -354,6 +354,10 @@ def prompt_pin(self): def send_pin(self, pin): raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') + # Toggle passphrase + def toggle_passphrase(self): + raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host') + def enumerate(password=''): results = [] devices = [] diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 1a9ac7edb..857d9f2fe 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -434,6 +434,12 @@ def send_pin(self, pin): return {'success': False} return {'success': True} + # Toggle passphrase + @trezor_exception + def toggle_passphrase(self): + self._check_unlocked() + return device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) + def enumerate(password=''): results = [] for dev in enumerate_devices(): diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index ea135b344..2667414b1 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -64,3 +64,7 @@ def prompt_pin(self): # Send pin def send_pin(self): raise NotImplementedError('The HardwareWalletClient base class does not implement this method') + + # Toggle passphrase + def toggle_passphrase(self): + raise NotImplementedError('The HardwareWalletClient base class does not implement this method') diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 1ae7e24a4..d6b209b7a 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -258,24 +258,8 @@ def test_pins(self): self.assertEqual(result['code'], -11) def test_passphrase(self): - # There's no passphrase - result = self.do_command(self.dev_args + ['enumerate']) - for dev in result: - if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': - self.assertFalse(dev['needs_passphrase_sent']) - self.assertEquals(dev['fingerprint'], '95d8f670') - # Setting a passphrase won't change the fingerprint - result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate']) - for dev in result: - if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': - self.assertFalse(dev['needs_passphrase_sent']) - self.assertEquals(dev['fingerprint'], '95d8f670') - - # Set a passphrase - device.wipe(self.client) - self.client.set_passphrase('pass') - load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test') - self.client.call(messages.ClearSession()) + # Enable passphrase + self.do_command(self.dev_args + ['togglepassphrase']) # A passphrase will need to be sent result = self.do_command(self.dev_args + ['enumerate']) @@ -302,6 +286,22 @@ def test_passphrase(self): self.assertFalse(dev['needs_passphrase_sent']) self.assertNotEqual(dev['fingerprint'], fpr) + # Disable passphrase + self.do_command(self.dev_args + ['togglepassphrase']) + + # There's no passphrase + result = self.do_command(self.dev_args + ['enumerate']) + for dev in result: + if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': + self.assertFalse(dev['needs_passphrase_sent']) + self.assertEquals(dev['fingerprint'], '95d8f670') + # Setting a passphrase won't change the fingerprint + result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate']) + for dev in result: + if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': + self.assertFalse(dev['needs_passphrase_sent']) + self.assertEquals(dev['fingerprint'], '95d8f670') + def keepkey_test_suite(emulator, rpc, userpass, interface): # Redirect stderr to /dev/null as it's super spammy sys.stderr = open(os.devnull, 'w') diff --git a/test/test_trezor.py b/test/test_trezor.py index 89e325b3f..e198b1b52 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -266,23 +266,8 @@ def test_pins(self): self.assertEqual(result['code'], -11) def test_passphrase(self): - # There's no passphrase - result = self.do_command(self.dev_args + ['enumerate']) - for dev in result: - if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': - self.assertFalse(dev['needs_passphrase_sent']) - self.assertEquals(dev['fingerprint'], '95d8f670') - # Setting a passphrase won't change the fingerprint - result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate']) - for dev in result: - if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': - self.assertFalse(dev['needs_passphrase_sent']) - self.assertEquals(dev['fingerprint'], '95d8f670') - - # Set a passphrase - device.wipe(self.client) - load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test') - self.client.call(messages.ClearSession()) + # Enable passphrase + self.do_command(self.dev_args + ['togglepassphrase']) # A passphrase will need to be sent result = self.do_command(self.dev_args + ['enumerate']) @@ -318,6 +303,22 @@ def test_passphrase(self): self.assertFalse(dev['needs_passphrase_sent']) self.assertNotEqual(dev['fingerprint'], fpr) + # Disable passphrase + self.do_command(self.dev_args + ['togglepassphrase']) + + # There's no passphrase + result = self.do_command(self.dev_args + ['enumerate']) + for dev in result: + if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': + self.assertFalse(dev['needs_passphrase_sent']) + self.assertEquals(dev['fingerprint'], '95d8f670') + # Setting a passphrase won't change the fingerprint + result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate']) + for dev in result: + if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': + self.assertFalse(dev['needs_passphrase_sent']) + self.assertEquals(dev['fingerprint'], '95d8f670') + def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): # Redirect stderr to /dev/null as it's super spammy sys.stderr = open(os.devnull, 'w') From a232005e73342b2fa448cddb7be6c3f87e2338c4 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 1 Apr 2020 11:29:43 -0400 Subject: [PATCH 087/634] getkeypool: --all option for descriptor wallets --- hwilib/cli.py | 8 ++++--- hwilib/commands.py | 58 +++++++++++++++++++++++++++++++-------------- test/test_device.py | 6 ----- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 2d77e976f..3f68f79f5 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -35,7 +35,7 @@ def getxpub_handler(args, client): return getxpub(client, path=args.path) def getkeypool_handler(args, client): - return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh) + return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh, addr_all=args.all) def getdescriptors_handler(args, client): return getdescriptors(client, account=args.account) @@ -137,8 +137,10 @@ def process_commands(cli_args): kparg_group.add_argument('--keypool', action='store_true', dest='keypool', help='Indicates that the keys are to be imported to the keypool', default=True) kparg_group.add_argument('--nokeypool', action='store_false', dest='keypool', help='Indicates that the keys are not to be imported to the keypool', default=False) getkeypool_parser.add_argument('--internal', action='store_true', help='Indicates that the keys are change keys') - getkeypool_parser.add_argument('--sh_wpkh', action='store_true', help='Generate p2sh-nested segwit addresses (default path: m/49h/0h/0h/[0,1]/*)') - getkeypool_parser.add_argument('--wpkh', action='store_true', help='Generate bech32 addresses (default path: m/84h/0h/0h/[0,1]/*)') + kp_type_group = getkeypool_parser.add_mutually_exclusive_group() + kp_type_group.add_argument('--sh_wpkh', action='store_true', help='Generate p2sh-nested segwit addresses (default path: m/49h/0h/0h/[0,1]/*)') + kp_type_group.add_argument('--wpkh', action='store_true', help='Generate bech32 addresses (default path: m/84h/0h/0h/[0,1]/*)') + kp_type_group.add_argument('--all', action='store_true', help='Generate addresses for all standard address types (default paths: m/{44,49,84}h/0h/0h/[0,1]/*)') getkeypool_parser.add_argument('--account', help='BIP43 account', type=int, default=0) getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. m/84h/0h/0h/1/* with --wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') getkeypool_parser.add_argument('start', type=int, help='The index to start at.') diff --git a/hwilib/commands.py b/hwilib/commands.py index d6ef823c1..5b1788031 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -10,6 +10,12 @@ from .errors import UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED from .descriptor import Descriptor from .devices import __all__ as all_devs +from enum import Enum + +class AddressType(Enum): + PKH = 1 + WPKH = 2 + SH_WPKH = 3 # Get the client for the device def get_client(device_type, device_path, password='', expert=False): @@ -80,16 +86,14 @@ def getxpub(client, path): def signmessage(client, message, path): return client.sign_message(message, path) -def getkeypool_inner(client, path, start, end, internal=False, keypool=True, account=0, sh_wpkh=False, wpkh=True): - if sh_wpkh and wpkh: - return {'error': 'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.', 'code': BAD_ARGUMENT} +def getkeypool_inner(client, path, start, end, internal=False, keypool=True, account=0, addr_type=AddressType.WPKH): try: master_fpr = client.get_master_fingerprint_hex() except NotImplementedError as e: return {'error': str(e), 'code': NOT_IMPLEMENTED} - desc = getdescriptor(client, master_fpr, client.is_testnet, path, internal, sh_wpkh, wpkh, account, start, end) + desc = getdescriptor(client, master_fpr, client.is_testnet, path, internal, addr_type, account, start, end) if not isinstance(desc, Descriptor): return desc @@ -105,19 +109,23 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import['watchonly'] = True return [this_import] -def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, sh_wpkh=False, wpkh=True, account=0, start=None, end=None): +def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, addr_type=AddressType.WPKH, account=0, start=None, end=None): testnet = client.is_testnet + is_wpkh = addr_type is AddressType.WPKH + is_sh_wpkh = addr_type is AddressType.SH_WPKH + if not path: # Master key: path = "m/" # Purpose - if wpkh: + if is_wpkh: path += "84'/" - elif sh_wpkh: + elif is_sh_wpkh: path += "49'/" else: + assert addr_type == AddressType.PKH path += "44'/" # Coin type @@ -153,21 +161,35 @@ def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, if client.xpub_cache.get(path_base) is None: client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base)['xpub'] - return Descriptor(master_fpr, path_base.replace('m', ''), client.xpub_cache.get(path_base), path_suffix, client.is_testnet, sh_wpkh, wpkh) + return Descriptor(master_fpr, path_base.replace('m', ''), client.xpub_cache.get(path_base), path_suffix, client.is_testnet, is_sh_wpkh, is_wpkh) + +def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, sh_wpkh=False, wpkh=True, addr_all=False): -# wrapper to allow both internal and external entries when path not given -def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, sh_wpkh=False, wpkh=True): + if sh_wpkh: + addr_types = [AddressType.SH_WPKH] + elif wpkh: + addr_types = [AddressType.WPKH] + elif addr_all: + addr_types = list(AddressType) + else: + addr_types = [AddressType.PKH] + + # When no specific path or internal-ness is specified, create standard types + chains = [] if path is None and not internal: - internal_chain = getkeypool_inner(client, None, start, end, True, keypool, account, sh_wpkh, wpkh) - external_chain = getkeypool_inner(client, None, start, end, False, keypool, account, sh_wpkh, wpkh) + for addr_type in addr_types: + for internal_addr in [False, True]: + chains = chains + getkeypool_inner(client, None, start, end, internal_addr, keypool, account, addr_type) + # Report the first error we encounter - for chain in [internal_chain, external_chain]: + for chain in chains: if 'error' in chain: return chain # No errors, return pair - return internal_chain + external_chain + return chains else: - return getkeypool_inner(client, path, start, end, internal, keypool, account, sh_wpkh, wpkh) + assert len(addr_types) == 1 + return getkeypool_inner(client, path, start, end, internal, keypool, account, addr_types[0]) def getdescriptors(client, account=0): @@ -180,9 +202,9 @@ def getdescriptors(client, account=0): for internal in [False, True]: descriptors = [] - desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=False, account=account) - desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=True, wpkh=False, account=account) - desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, sh_wpkh=False, wpkh=True, account=account) + desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.PKH, account=account) + desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.SH_WPKH, account=account) + desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.WPKH, account=account) for desc in [desc1, desc2, desc3]: if not isinstance(desc, Descriptor): return desc diff --git a/test/test_device.py b/test/test_device.py index 31be6a412..4c0154f46 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -175,12 +175,6 @@ def setUp(self): self.dev_args.append('--testnet') self.emulator.start() - def test_getkeypool_bad_args(self): - result = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--wpkh', '0', '20']) - self.assertIn('error', result) - self.assertIn('code', result) - self.assertEqual(result['code'], -7) - def test_getkeypool(self): non_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--nokeypool', '0', '20']) import_result = self.wpk_rpc.importmulti(non_keypool_desc) From 15b213a6dc0e9193fc624973b22a411723cf1974 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Fri, 3 Apr 2020 12:18:56 -0400 Subject: [PATCH 088/634] add test for getkeypool --all --- test/test_device.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 4c0154f46..068a6699d 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -180,11 +180,11 @@ def test_getkeypool(self): import_result = self.wpk_rpc.importmulti(non_keypool_desc) self.assertTrue(import_result[0]['success']) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '0', '20']) - import_result = self.wpk_rpc.importmulti(keypool_desc) + pkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '0', '20']) + import_result = self.wpk_rpc.importmulti(pkh_keypool_desc) self.assertFalse(import_result[0]['success']) - import_result = self.wrpc.importmulti(keypool_desc) + import_result = self.wrpc.importmulti(pkh_keypool_desc) self.assertTrue(import_result[0]['success']) for i in range(0, 21): addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) @@ -192,8 +192,8 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) self.assertEqual(addr_info['hdkeypath'], "m/44'/1'/0'/1/{}".format(i)) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '0', '20']) - import_result = self.wrpc.importmulti(keypool_desc) + shwpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '0', '20']) + import_result = self.wrpc.importmulti(shwpkh_keypool_desc) self.assertTrue(import_result[0]['success']) for i in range(0, 21): addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit')) @@ -201,8 +201,8 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/1/{}".format(i)) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20']) - import_result = self.wrpc.importmulti(keypool_desc) + wpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20']) + import_result = self.wrpc.importmulti(wpkh_keypool_desc) self.assertTrue(import_result[0]['success']) for i in range(0, 21): addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) @@ -210,6 +210,10 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/0'/1/{}".format(i)) + # Test that `--all` option gives the "concatenation" of previous three calls + all_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '0', '20']) + self.assertEqual(all_keypool_desc, pkh_keypool_desc + wpkh_keypool_desc + shwpkh_keypool_desc) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--account', '3', '0', '20']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) From 48498105e9c5fee0270ff305313c83ea2f0da63f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 16 Apr 2020 19:15:07 -0400 Subject: [PATCH 089/634] trezor: use GetPublicNode instead of Ping for promptpin Ping no long has the option to prompt for the pin. Instead use GetPublicNode which will always prompt for a pin. --- hwilib/devices/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 72c2a15a7..a3203bdbb 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -414,7 +414,7 @@ def prompt_pin(self): raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - self.client.call_raw(proto.Ping(message=b'ping', button_protection=False, pin_protection=True, passphrase_protection=False)) + self.client.call_raw(proto.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=None, script_type=proto.InputScriptType.SPENDADDRESS)) return {'success': True} # Send the pin From c3c6963b0074822ad9da94963f492bb43593cfc9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 16 Apr 2020 19:15:39 -0400 Subject: [PATCH 090/634] trezorlib: Initialize needs to be sent if there is no active session --- hwilib/devices/trezorlib/client.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index b64e1c13b..e19984106 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -190,6 +190,16 @@ def init_device(self): resp = self.call_raw(messages.Initialize()) if not isinstance(resp, messages.Features): raise exceptions.TrezorException("Unexpected initial response") + # For the T, we need to check if a passphrase needs to be entered + elif resp.model == 'T': + # Try GetPublicKey. If it fails, we try to send Initialize + pubkey_resp = self.call_raw(messages.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000])) + if isinstance(pubkey_resp, messages.Failure): + resp = self.call_raw(messages.Initialize()) + if not isinstance(resp, messages.Features): + raise exceptions.TrezorException("Unexpected initial response") + elif isinstance(pubkey_resp, messages.PassphraseRequest): + self.call_raw(messages.Cancel()) self.features = resp if self.features.vendor not in VENDORS: raise RuntimeError("Unsupported device") From 2862be4800c64e57bdf8151decdfc06e96dedbe1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 16 Apr 2020 19:17:04 -0400 Subject: [PATCH 091/634] trezor: Update passphrase workflow and messages --- hwilib/devices/trezor.py | 2 +- hwilib/devices/trezorlib/client.py | 48 +++++++++++-------- hwilib/devices/trezorlib/device.py | 9 ++-- .../trezorlib/messages/ApplySettings.py | 9 ++-- ...ck.py => Deprecated_PassphraseStateAck.py} | 2 +- ...y => Deprecated_PassphraseStateRequest.py} | 8 ++-- hwilib/devices/trezorlib/messages/Features.py | 6 +++ .../devices/trezorlib/messages/MessageType.py | 4 +- .../trezorlib/messages/PassphraseAck.py | 9 ++-- .../trezorlib/messages/PassphraseRequest.py | 6 +-- hwilib/devices/trezorlib/messages/__init__.py | 4 +- 11 files changed, 66 insertions(+), 41 deletions(-) rename hwilib/devices/trezorlib/messages/{PassphraseStateAck.py => Deprecated_PassphraseStateAck.py} (66%) rename hwilib/devices/trezorlib/messages/{PassphraseStateRequest.py => Deprecated_PassphraseStateRequest.py} (58%) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index a3203bdbb..383006015 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -467,7 +467,7 @@ def enumerate(password=''): if client.client.features.model == '1': d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection # always need the passphrase sent for Trezor One if it has passphrase protection enabled else: - d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection and not client.client.features.passphrase_cached + d_data['needs_passphrase_sent'] = False if d_data['needs_pin_sent']: raise DeviceNotReadyError('Trezor is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') if d_data['needs_passphrase_sent'] and not password: diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index e19984106..2abb87264 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -130,27 +130,37 @@ def _callback_pin(self, msg): return resp def _callback_passphrase(self, msg): - if msg.on_device: - passphrase = None - else: - try: - passphrase = self.ui.get_passphrase() - except: - self.call_raw(messages.Cancel()) - raise - - passphrase = Mnemonic.normalize_string(passphrase) - if len(passphrase) > MAX_PASSPHRASE_LENGTH: - self.call_raw(messages.Cancel()) - raise ValueError("Passphrase too long") - - resp = self.call_raw(messages.PassphraseAck(passphrase=passphrase)) - if isinstance(resp, messages.PassphraseStateRequest): - self.state = resp.state - return self.call_raw(messages.PassphraseStateAck()) - else: + available_on_device = self.features.model == 'T' + + def send_passphrase(passphrase=None, on_device=None): + msg = messages.PassphraseAck(passphrase=passphrase, on_device=on_device) + resp = self.call_raw(msg) + if isinstance(resp, messages.Deprecated_PassphraseStateRequest): + self.session_id = resp._state + resp = self.call_raw(messages.Deprecated_PassphraseStateAck()) return resp + # short-circuit old style entry + if msg._on_device is True: + return send_passphrase(None, None) + + if available_on_device: + return send_passphrase(on_device=True) + + try: + passphrase = self.ui.get_passphrase() + except: + self.call_raw(messages.Cancel()) + raise + + # else process host-entered passphrase + passphrase = Mnemonic.normalize_string(passphrase) + if len(passphrase) > MAX_PASSPHRASE_LENGTH: + self.call_raw(messages.Cancel()) + raise ValueError("Passphrase too long") + + return send_passphrase(passphrase=passphrase) + def _callback_button(self, msg): __tracebackhide__ = True # for pytest # pylint: disable=W0612 # do this raw - send ButtonAck first, notify UI later diff --git a/hwilib/devices/trezorlib/device.py b/hwilib/devices/trezorlib/device.py index 7773f0f2a..329e79df7 100644 --- a/hwilib/devices/trezorlib/device.py +++ b/hwilib/devices/trezorlib/device.py @@ -51,8 +51,9 @@ def apply_settings( language=None, use_passphrase=None, homescreen=None, - passphrase_source=None, + passphrase_always_on_device=None, auto_lock_delay_ms=None, + display_rotation=None, ): settings = proto.ApplySettings() if label is not None: @@ -63,10 +64,12 @@ def apply_settings( settings.use_passphrase = use_passphrase if homescreen is not None: settings.homescreen = homescreen - if passphrase_source is not None: - settings.passphrase_source = passphrase_source + if passphrase_always_on_device is not None: + settings.passphrase_always_on_device = passphrase_always_on_device if auto_lock_delay_ms is not None: settings.auto_lock_delay_ms = auto_lock_delay_ms + if display_rotation is not None: + settings.display_rotation = display_rotation out = client.call(settings) client.init_device() # Reload Features diff --git a/hwilib/devices/trezorlib/messages/ApplySettings.py b/hwilib/devices/trezorlib/messages/ApplySettings.py index 3330a3136..26b39a288 100644 --- a/hwilib/devices/trezorlib/messages/ApplySettings.py +++ b/hwilib/devices/trezorlib/messages/ApplySettings.py @@ -12,15 +12,17 @@ def __init__( label: str = None, use_passphrase: bool = None, homescreen: bytes = None, - passphrase_source: int = None, auto_lock_delay_ms: int = None, + display_rotation: int = None, + passphrase_always_on_device: bool = None, ) -> None: self.language = language self.label = label self.use_passphrase = use_passphrase self.homescreen = homescreen - self.passphrase_source = passphrase_source self.auto_lock_delay_ms = auto_lock_delay_ms + self.display_rotation = display_rotation + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls): @@ -29,6 +31,7 @@ def get_fields(cls): 2: ('label', p.UnicodeType, 0), 3: ('use_passphrase', p.BoolType, 0), 4: ('homescreen', p.BytesType, 0), - 5: ('passphrase_source', p.UVarintType, 0), 6: ('auto_lock_delay_ms', p.UVarintType, 0), + 7: ('display_rotation', p.UVarintType, 0), + 8: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/hwilib/devices/trezorlib/messages/PassphraseStateAck.py b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py similarity index 66% rename from hwilib/devices/trezorlib/messages/PassphraseStateAck.py rename to hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py index 7563e61ed..475b9d2c6 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseStateAck.py +++ b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py @@ -3,5 +3,5 @@ from .. import protobuf as p -class PassphraseStateAck(p.MessageType): +class Deprecated_PassphraseStateAck(p.MessageType): MESSAGE_WIRE_TYPE = 78 diff --git a/hwilib/devices/trezorlib/messages/PassphraseStateRequest.py b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py similarity index 58% rename from hwilib/devices/trezorlib/messages/PassphraseStateRequest.py rename to hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py index 92dd4e9da..aa9a54681 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseStateRequest.py +++ b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py @@ -3,17 +3,17 @@ from .. import protobuf as p -class PassphraseStateRequest(p.MessageType): +class Deprecated_PassphraseStateRequest(p.MessageType): MESSAGE_WIRE_TYPE = 77 def __init__( self, - state: bytes = None, + _state: bytes = None, ) -> None: - self.state = state + self._state = _state @classmethod def get_fields(cls): return { - 1: ('state', p.BytesType, 0), + 1: ('_state', p.BytesType, 0), } diff --git a/hwilib/devices/trezorlib/messages/Features.py b/hwilib/devices/trezorlib/messages/Features.py index c6aff5902..93a681d0a 100644 --- a/hwilib/devices/trezorlib/messages/Features.py +++ b/hwilib/devices/trezorlib/messages/Features.py @@ -35,6 +35,8 @@ def __init__( fw_vendor_keys: bytes = None, unfinished_backup: bool = None, no_backup: bool = None, + session_id: bytes = None, + passphrase_always_on_device: bool = None, ) -> None: self.vendor = vendor self.major_version = major_version @@ -63,6 +65,8 @@ def __init__( self.fw_vendor_keys = fw_vendor_keys self.unfinished_backup = unfinished_backup self.no_backup = no_backup + self.session_id = session_id + self.passphrase_always_on_device = passphrase_always_on_device @classmethod def get_fields(cls): @@ -94,4 +98,6 @@ def get_fields(cls): # 26: ('fw_vendor_keys', p.BytesType, 0), # 27: ('unfinished_backup', p.BoolType, 0), # 28: ('no_backup', p.BoolType, 0), + 35: ('session_id', p.BytesType, 0), + 36: ('passphrase_always_on_device', p.BoolType, 0), } diff --git a/hwilib/devices/trezorlib/messages/MessageType.py b/hwilib/devices/trezorlib/messages/MessageType.py index 852ba8c21..d6ebc1743 100644 --- a/hwilib/devices/trezorlib/messages/MessageType.py +++ b/hwilib/devices/trezorlib/messages/MessageType.py @@ -24,8 +24,8 @@ EntropyAck = 36 PassphraseRequest = 41 PassphraseAck = 42 -PassphraseStateRequest = 77 -PassphraseStateAck = 78 +Deprecated_PassphraseStateRequest = 77 +Deprecated_PassphraseStateAck = 78 RecoveryDevice = 45 WordRequest = 46 WordAck = 47 diff --git a/hwilib/devices/trezorlib/messages/PassphraseAck.py b/hwilib/devices/trezorlib/messages/PassphraseAck.py index 8f49ce239..993aa0eb7 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseAck.py +++ b/hwilib/devices/trezorlib/messages/PassphraseAck.py @@ -9,14 +9,17 @@ class PassphraseAck(p.MessageType): def __init__( self, passphrase: str = None, - state: bytes = None, + _state: bytes = None, + on_device: bool = None, ) -> None: self.passphrase = passphrase - self.state = state + self._state = _state + self.on_device = on_device @classmethod def get_fields(cls): return { 1: ('passphrase', p.UnicodeType, 0), - 2: ('state', p.BytesType, 0), + 2: ('_state', p.BytesType, 0), + 3: ('on_device', p.BoolType, 0), } diff --git a/hwilib/devices/trezorlib/messages/PassphraseRequest.py b/hwilib/devices/trezorlib/messages/PassphraseRequest.py index 919fee9c3..b4f09efe3 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseRequest.py +++ b/hwilib/devices/trezorlib/messages/PassphraseRequest.py @@ -8,12 +8,12 @@ class PassphraseRequest(p.MessageType): def __init__( self, - on_device: bool = None, + _on_device: bool = None, ) -> None: - self.on_device = on_device + self._on_device = _on_device @classmethod def get_fields(cls): return { - 1: ('on_device', p.BoolType, 0), + 1: ('_on_device', p.BoolType, 0), } diff --git a/hwilib/devices/trezorlib/messages/__init__.py b/hwilib/devices/trezorlib/messages/__init__.py index d01eb4f7f..9a4ad93da 100644 --- a/hwilib/devices/trezorlib/messages/__init__.py +++ b/hwilib/devices/trezorlib/messages/__init__.py @@ -40,8 +40,8 @@ from .MultisigRedeemScriptType import MultisigRedeemScriptType from .PassphraseAck import PassphraseAck from .PassphraseRequest import PassphraseRequest -from .PassphraseStateAck import PassphraseStateAck -from .PassphraseStateRequest import PassphraseStateRequest +from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck +from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest from .PinMatrixAck import PinMatrixAck from .PinMatrixRequest import PinMatrixRequest from .Ping import Ping From 32989bb03a46b7518f62cb152f34e554ab543a8d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 16 Apr 2020 20:36:37 -0400 Subject: [PATCH 092/634] tests, trezor: Go back to using the master branch of trezor-firmware --- test/setup_environment.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index f1da1e0d6..acb9af26f 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -12,7 +12,6 @@ trezor_setup_needed=false if [ ! -d "trezor-firmware" ]; then git clone --recursive https://github.com/trezor/trezor-firmware.git cd trezor-firmware - git checkout core/v2.2.0 trezor_setup_needed=true else cd trezor-firmware @@ -55,6 +54,8 @@ make build_unix # Delete any emulator.img file rm /var/tmp/trezor.flash cd ../.. +# Remove nanopb to avoid interfering with keepkey +pip uninstall -y nanopb # Clone coldcard firmware if it doesn't exist, or update it if it does coldcard_setup_needed=false From baf9090ce7940c73c3f640d66043436b3f5de648 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 17 Apr 2020 21:40:05 -0400 Subject: [PATCH 093/634] tests: Update speculos patch --- test/data/speculos-auto-button.patch | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch index db9741d8a..cb3c1e5b0 100644 --- a/test/data/speculos-auto-button.patch +++ b/test/data/speculos-auto-button.patch @@ -1,4 +1,4 @@ -From b3156bb4705a55dd03fe40465bd5574c472ba335 Mon Sep 17 00:00:00 2001 +From 80038049e33f6a708e1d1c2151ef48e1bce3ee70 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 17:25:09 -0400 Subject: [PATCH] Do button presses within seproxyhal @@ -8,7 +8,7 @@ Subject: [PATCH] Do button presses within seproxyhal 1 file changed, 35 insertions(+) diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py -index fffba52..462456a 100644 +index b18f8a2..b0554ea 100644 --- a/mcu/seproxyhal.py +++ b/mcu/seproxyhal.py @@ -2,6 +2,7 @@ import binascii @@ -19,7 +19,7 @@ index fffba52..462456a 100644 import sys import time import threading -@@ -129,6 +130,7 @@ class SeProxyHal: +@@ -146,6 +147,7 @@ class SeProxyHal: daemon=True) self.ticker_thread.start() self.usb = usb.USB(self.packet_thread.queue_packet) @@ -27,9 +27,9 @@ index fffba52..462456a 100644 def _recvall(self, size): data = b'' -@@ -168,6 +170,39 @@ class SeProxyHal: - #self.logger.debug(f"DISPLAY_STATUS {data!r}") - screen.display_status(data) +@@ -248,6 +250,39 @@ class SeProxyHal: + self.logger.debug(f"DISPLAY_STATUS {data!r}") + ret = screen.display_status(data) self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) + data_type, _, x, y, = struct.unpack('bbhh', data[:6]) + if data_type == 7: @@ -68,5 +68,5 @@ index fffba52..462456a 100644 elif tag == SephTag.SCREEN_DISPLAY_RAW_STATUS: self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") -- -2.25.2 +2.26.1 From f2fe7b82d9af18adba2249f98908a874bdaca42c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 20 Apr 2020 13:10:16 -0400 Subject: [PATCH 094/634] build: import subprocess in hwi-qt.spec --- hwi-qt.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/hwi-qt.spec b/hwi-qt.spec index ef34360ca..b0e841873 100644 --- a/hwi-qt.spec +++ b/hwi-qt.spec @@ -1,6 +1,7 @@ # -*- mode: python ; coding: utf-8 -*- import platform +import subprocess block_cipher = None From 2d5cc8fa80f925f06d6796ba0776599f3043299b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 20 Apr 2020 14:57:18 -0400 Subject: [PATCH 095/634] release: bump to version 1.1.0 and regenerate setup.py --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index 3f6fab60f..1a72d32e5 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.0.3' +__version__ = '1.1.0' diff --git a/pyproject.toml b/pyproject.toml index a989b0bdb..675db3021 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.0.3" +version = "1.1.0" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index 2ec1fcd26..40b373ab3 100644 --- a/setup.py +++ b/setup.py @@ -29,9 +29,9 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.0.3', + 'version': '1.1.0', 'description': 'A library for working with Bitcoin hardware wallets', - 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system) required for operation and development. See `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies.\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | No | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", + 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', 'author_email': 'andrew@achow101.com', 'maintainer': None, From 57633c6114fdb6075269637531290cdf9b4747f2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 21 Apr 2020 10:39:40 +0300 Subject: [PATCH 096/634] Fix macOS hwi-qt build --- contrib/generate-ui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh index 349b6a544..980a87c24 100755 --- a/contrib/generate-ui.sh +++ b/contrib/generate-ui.sh @@ -5,6 +5,6 @@ for file in *.ui do gen_file=ui_`echo $file| cut -d. -f1`.py pyside2-uic $file -o $gen_file - sed -i 's/raise()/raise_()/g' $gen_file + sed -i '' -e 's/raise()/raise_()/g' $gen_file done popd From ff6cdfa8630ce6feba2a203d4304e8b89dc9857b Mon Sep 17 00:00:00 2001 From: benk10 Date: Sun, 26 Apr 2020 17:12:08 +0300 Subject: [PATCH 097/634] Fix Keepkey `togglepassphrase` Instead of returning an error, ask the user to use `sendpin` after calling `apply_settings` on Keepkey. --- hwilib/devices/trezor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 383006015..323b250f1 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -438,7 +438,14 @@ def send_pin(self, pin): @trezor_exception def toggle_passphrase(self): self._check_unlocked() - return device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) + try: + device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) + except: + if self.type == 'Keepkey': + print('Confirm the action by entering your PIN', file=sys.stderr) + print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) + print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) + return {'success': True} def enumerate(password=''): results = [] From 0075e7fde2c9bf904035490bdefe2399a6e3bfd8 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sun, 26 Apr 2020 17:38:54 +0300 Subject: [PATCH 098/634] Add `togglepassphrase` to GUI Add "Toggle Passphrase" button to the GUI, enabled for Trezor and Keepkey devices. --- hwilib/gui.py | 22 +++++++++++++++++----- hwilib/ui/mainwindow.ui | 10 ++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index 61d59cd69..dcdfeabf0 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -47,7 +47,7 @@ def __init__(self): class SendPinDialog(QDialog): pin_sent_success = Signal() - def __init__(self, client): + def __init__(self, client, prompt_pin=True): super(SendPinDialog, self).__init__() self.ui = Ui_SendPinDialog() self.ui.setupUi(self) @@ -68,7 +68,8 @@ def __init__(self, client): self.ui.p9_button.clicked.connect(self.button_clicked(9)) self.accepted.connect(self.sendpindialog_accepted) - do_command(commands.prompt_pin, self.client) + if prompt_pin: + do_command(commands.prompt_pin, self.client) def button_clicked(self, number): @Slot() @@ -233,12 +234,13 @@ def __init__(self, passphrase='', testnet=False): self.ui.enumerate_refresh_button.clicked.connect(self.refresh_clicked) self.ui.setpass_button.clicked.connect(self.show_setpassphrasedialog) - self.ui.sendpin_button.clicked.connect(self.show_sendpindialog) + self.ui.sendpin_button.clicked.connect(lambda: self.show_sendpindialog(prompt_pin=True)) self.ui.getxpub_button.clicked.connect(self.show_getxpubdialog) self.ui.signtx_button.clicked.connect(self.show_signpsbtdialog) self.ui.signmsg_button.clicked.connect(self.show_signmessagedialog) self.ui.display_addr_button.clicked.connect(self.show_displayaddressdialog) self.ui.getkeypool_opts_button.clicked.connect(self.show_getkeypooloptionsdialog) + self.ui.toggle_passphrase_button.clicked.connect(self.toggle_passphrase) self.ui.enumerate_combobox.currentIndexChanged.connect(self.get_client_and_device_info) @@ -248,6 +250,7 @@ def clear_info(self): self.ui.signmsg_button.setEnabled(False) self.ui.display_addr_button.setEnabled(False) self.ui.getkeypool_opts_button.setEnabled(False) + self.ui.toggle_passphrase_button.setEnabled(False) self.ui.keypool_textedit.clear() self.ui.desc_textedit.clear() @@ -298,6 +301,9 @@ def get_client_and_device_info(self, index): self.device_info = self.devices[index - 1] self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) self.client.is_testnet = self.testnet + + self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] == 'trezor' or self.device_info['type'] == 'keepkey') + self.get_device_info() def get_device_info(self): @@ -331,8 +337,8 @@ def get_device_info(self): self.ui.desc_textedit.setPlainText(json.dumps(descriptors, indent=2)) @Slot() - def show_sendpindialog(self): - self.current_dialog = SendPinDialog(self.client) + def show_sendpindialog(self, prompt_pin=True): + self.current_dialog = SendPinDialog(self.client, prompt_pin) self.current_dialog.pin_sent_success.connect(self.sendpindialog_accepted) self.current_dialog.exec_() @@ -387,6 +393,12 @@ def getkeypooloptionsdialog_accepted(self): self.current_dialog = None self.get_device_info() + @Slot() + def toggle_passphrase(self): + do_command(commands.toggle_passphrase, self.client) + if self.device_info['model'] == "keepkey": + self.show_sendpindialog(prompt_pin=False) + def process_gui_commands(cli_args): parser = HWIArgumentParser(description='Hardware Wallet Interface Qt, version {}.\nInteractively access and send commands to a hardware wallet device with a GUI. Responses are in JSON format.'.format(__version__)) parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') diff --git a/hwilib/ui/mainwindow.ui b/hwilib/ui/mainwindow.ui index f22028f9f..a27955b4d 100644 --- a/hwilib/ui/mainwindow.ui +++ b/hwilib/ui/mainwindow.ui @@ -177,6 +177,16 @@ + + + + false + + + Toggle Passphrase + + + From d85be9eddee463598dbe7eec62988e78fa31047a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 8 May 2020 13:14:06 -0400 Subject: [PATCH 099/634] serialization: put missing final_scriptwitness length The length of a final_scriptwitness value was missing. Put it back in. --- hwilib/serializations.py | 3 ++- test/data/test_psbt.json | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 21979e341..e76d5f291 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -612,7 +612,8 @@ def serialize(self): if not self.final_script_witness.is_null(): r += ser_string(b"\x08") - r += self.final_script_witness.serialize() + witstack = self.final_script_witness.serialize() + r += ser_string(witstack) for key, value in sorted(self.unknown.items()): r += ser_string(key) diff --git a/test/data/test_psbt.json b/test/data/test_psbt.json index 4a3d47fd7..39dccdbca 100644 --- a/test/data/test_psbt.json +++ b/test/data/test_psbt.json @@ -25,7 +25,8 @@ "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" ], "creator" : [ { @@ -96,4 +97,4 @@ "result" : "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" } ] -} \ No newline at end of file +} From 3c0eaccd79d57f538fcc7cada2a53aaf6ffbc800 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 15 May 2020 21:13:38 -0400 Subject: [PATCH 100/634] Rename 'l' variables to silence flake8 E741 --- hwilib/serializations.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 21979e341..b6840a886 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -40,16 +40,16 @@ def hash160(s): # Serialization/deserialization tools -def ser_compact_size(l): +def ser_compact_size(size): r = b"" - if l < 253: - r = struct.pack("B", l) - elif l < 0x10000: - r = struct.pack(" Date: Tue, 26 May 2020 13:09:02 -0400 Subject: [PATCH 101/634] Strip all local symbols to avoid xcode complaint --- contrib/build_bin.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index c486b9b32..a502a36c4 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -11,7 +11,7 @@ poetry install # We now need to remove debugging symbols and build id from the hidapi SO file so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages -strip ${so_dir}/hid*.so +strip -x ${so_dir}/hid*.so if [[ $OSTYPE != *"darwin"* ]]; then strip -R .note.gnu.build-id ${so_dir}/hid*.so fi From 7bfcff7b776bbaa46c40757af7f15789919c863b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 15 May 2020 21:23:16 -0400 Subject: [PATCH 102/634] Update dependencies --- poetry.lock | 225 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 164 insertions(+), 61 deletions(-) diff --git a/poetry.lock b/poetry.lock index b15464142..3e33e40e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ description = "Python graph (network) package" name = "altgraph" optional = false python-versions = "*" -version = "0.16.1" +version = "0.17" [[package]] category = "dev" @@ -12,40 +12,43 @@ description = "A tool that automatically formats Python code to conform to the P name = "autopep8" optional = false python-versions = "*" -version = "1.4.4" +version = "1.5.2" [package.dependencies] -pycodestyle = ">=2.4.0" +pycodestyle = ">=2.5.0" [[package]] -category = "main" -description = "ECDSA cryptographic signature library (pure python)" -name = "ecdsa" +category = "dev" +description = "Python 2.7 backport of the \"dis\" module from Python 3.5+" +name = "dis3" optional = false python-versions = "*" -version = "0.13.2" +version = "0.1.3" [[package]] -category = "dev" -description = "Discover and load entry points from installed packages." -name = "entrypoints" +category = "main" +description = "ECDSA cryptographic signature library (pure python)" +name = "ecdsa" optional = false -python-versions = ">=2.7" -version = "0.3" +python-versions = "*" +version = "0.13.3" [[package]] category = "dev" -description = "the modular source code checker: pep8, pyflakes and co" +description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.8" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.1" [package.dependencies] -entrypoints = ">=0.3.0,<0.4.0" mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.5.0,<2.6.0" -pyflakes = ">=2.1.0,<2.2.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" [[package]] category = "dev" @@ -54,7 +57,7 @@ marker = "sys_platform == \"win32\"" name = "future" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" +version = "0.18.2" [[package]] category = "main" @@ -67,13 +70,29 @@ version = "0.7.99.post21" [package.dependencies] setuptools = ">=19.0" +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + [[package]] category = "main" description = "Pure-python wrapper for libusb-1.0" name = "libusb1" optional = false python-versions = "*" -version = "1.7.1" +version = "1.8" [[package]] category = "dev" @@ -82,7 +101,7 @@ marker = "sys_platform == \"darwin\"" name = "macholib" optional = false python-versions = "*" -version = "1.11" +version = "1.14" [package.dependencies] altgraph = ">=0.15" @@ -140,7 +159,7 @@ description = "Python style guide checker" name = "pycodestyle" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.5.0" +version = "2.6.0" [[package]] category = "dev" @@ -148,18 +167,19 @@ description = "passive checker of Python programs" name = "pyflakes" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.1.1" +version = "2.2.0" [[package]] category = "dev" description = "PyInstaller bundles a Python application and all its dependencies into a single package." name = "pyinstaller" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.6" [package.dependencies] altgraph = "*" +dis3 = "*" setuptools = "*" [[package]] @@ -168,10 +188,10 @@ description = "Python bindings for the Qt cross-platform application and UI fram name = "pyside2" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.14.0" +version = "5.14.2.1" [package.dependencies] -shiboken2 = "5.14.0" +shiboken2 = "5.14.2.1" [[package]] category = "dev" @@ -188,51 +208,134 @@ description = "Python / C++ bindings helper module" name = "shiboken2" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.14.0" +version = "5.14.2.1" [[package]] category = "main" -description = "Type Hints for Python" -name = "typing" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.1" +version = "3.7.4.2" [[package]] -category = "main" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" optional = false -python-versions = "*" -version = "3.7.4" +python-versions = ">=3.6" +version = "3.1.0" -[package.dependencies] -typing = ">=3.7.4" +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] [metadata] content-hash = "e076be1de3ff88d71f7ed62865dc7f4ecc424ef5d409cc1b7db578021961164a" python-versions = "^3.6,<3.9" -[metadata.hashes] -altgraph = ["d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", "ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"] -autopep8 = ["4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee"] -ecdsa = ["20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c", "5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884"] -entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] -flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] -future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] -hidapi = ["1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24", "6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3", "8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946", "92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7", "b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87", "bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660", "c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7", "d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa", "d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b", "e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97", "edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922"] -libusb1 = ["adf64a4f3f5c94643a1286f8153bcf4bc787c348b38934aacd7fe17fbeebc571"] -macholib = ["ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1", "c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"] -mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] -mnemonic = ["02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"] -pbkdf2 = ["ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"] -pefile = ["a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"] -pyaes = ["02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"] -pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] -pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] -pyinstaller = ["ee7504022d1332a3324250faf2135ea56ac71fdb6309cff8cd235de26b1d0a96"] -pyside2 = ["11bba54a62bcd9d7879d3e74cc54c0054c8c6dcdf011ecee9b47c5229cbd7af9", "578b727a5a254cfd509ea2f1fa31779f217a2a1d765c770727662dac950d60eb", "72feeb655958791383085bcb3154f6b3e193c1d66b6aa771c4244a6cafd62b7e", "77474e11c0bb3efa2d7e8506fe0f36049585ba911b8242e070b5f8978e5ba6f7", "c9f59e8c49a9a3b0cca04d8468becd8a562eb9ad0ac1d4d9a8622d2dfa3ce4c9", "ce43f98333443242cd3fe976d72fcb3acf6bb7fa40dd5949e59947a501d5dd72"] -pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] -shiboken2 = ["101fc366798f88cbff13586907f3755bebbd9304e66626fb6b0f6b28e0c9a5d2", "47c7c2652f578b37588e8b6daff3a852b3c88ae0f83be13886e4a74859e81763", "4f138656fc755399776062c89492d61f887d4e5fe7c78cded73917e80afcf2f5", "676fef81e4d95b02816fde7359c1f2604efa3edd34b05ab0da42c57c7555f7d7", "a88267c7cc17501effc6b1b36d85e7ab28173af939b975ea42716ed12493b478", "ab3ba84784c9641a11a21a8c64d494fa8b57be25e081e77f76747d543699f03c"] -typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] -typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] +[metadata.files] +altgraph = [ + {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"}, + {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, +] +autopep8 = [ + {file = "autopep8-1.5.2.tar.gz", hash = "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"}, +] +dis3 = [ + {file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"}, + {file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"}, + {file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"}, +] +ecdsa = [ + {file = "ecdsa-0.13.3-py2.py3-none-any.whl", hash = "sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db"}, + {file = "ecdsa-0.13.3.tar.gz", hash = "sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d"}, +] +flake8 = [ + {file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"}, + {file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +hidapi = [ + {file = "hidapi-0.7.99.post21-cp27-cp27m-win32.whl", hash = "sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660"}, + {file = "hidapi-0.7.99.post21-cp27-cp27m-win_amd64.whl", hash = "sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24"}, + {file = "hidapi-0.7.99.post21-cp34-cp34m-win32.whl", hash = "sha256:d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa"}, + {file = "hidapi-0.7.99.post21-cp34-cp34m-win_amd64.whl", hash = "sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7"}, + {file = "hidapi-0.7.99.post21-cp35-cp35m-win32.whl", hash = "sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b"}, + {file = "hidapi-0.7.99.post21-cp35-cp35m-win_amd64.whl", hash = "sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87"}, + {file = "hidapi-0.7.99.post21-cp36-cp36m-win32.whl", hash = "sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922"}, + {file = "hidapi-0.7.99.post21-cp36-cp36m-win_amd64.whl", hash = "sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946"}, + {file = "hidapi-0.7.99.post21-cp37-cp37m-win32.whl", hash = "sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7"}, + {file = "hidapi-0.7.99.post21-cp37-cp37m-win_amd64.whl", hash = "sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3"}, + {file = "hidapi-0.7.99.post21.tar.gz", hash = "sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] +libusb1 = [ + {file = "libusb1-1.8.tar.gz", hash = "sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6"}, +] +macholib = [ + {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, + {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mnemonic = [ + {file = "mnemonic-0.18.tar.gz", hash = "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"}, +] +pbkdf2 = [ + {file = "pbkdf2-1.3.tar.gz", hash = "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"}, +] +pefile = [ + {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, +] +pyaes = [ + {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pyinstaller = [ + {file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"}, +] +pyside2 = [ + {file = "PySide2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:71d0416e69f0ac4d5d0f9892c819b2896a4e821bc83b29932769060119f3292c"}, + {file = "PySide2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d5054009ced176cf78d71d2787bdcd676bc990004bec0e7079f7bdfe7edffe5"}, + {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:a6390619b9c8713ba190dd3fa116a2d9f63597cb0f902fbf31b2657023936a3a"}, + {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:174e1863ae3526bae8ae946b24cccb472dffd7e643bc47ae4d2de39cac583a9c"}, + {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:e21058fcd8d2cb9871fc61f9d35ed15f0e0c4718c5d463a2e37be1d67b8c40b4"}, + {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:93c19beef80ef54392e6cd0de937be4e1603560229eb38738c8b50bbb8da90f7"}, +] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] +shiboken2 = [ + {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d285d476a76f254bff69cc58c1d4385df295b42de1a818d4a8d11694c2d728fc"}, + {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73d03e74f542204e351539e42ab3e3727a69408e1497af4c6e84fb66c3e706d8"}, + {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:16b59490228bf923ea7c8ed6edcb4f7349ce5a5fc30369190c41487baf6d4aaa"}, + {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:cd6ba0ba0d070c8ec090ad3eb10440989f7e5a4404c6b087f8f695a75a01e1dc"}, + {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:fe4d0cf6737f1d01944be4cf3b401d74015c515ab84622bf04f47d64ffcd39f9"}, + {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:c022203b7cf01df6ad0bb190d286c2965958243a16e47bee8c5e6bbb9d0cd475"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] From f8b40cdcd08fa9207a115462de3c1d8ee36a4d6a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 15 May 2020 21:24:19 -0400 Subject: [PATCH 103/634] Add qt extra to make pyside2 optional --- poetry.lock | 9 ++++++--- pyproject.toml | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3e33e40e3..f35a1d742 100644 --- a/poetry.lock +++ b/poetry.lock @@ -186,7 +186,7 @@ setuptools = "*" category = "main" description = "Python bindings for the Qt cross-platform application and UI framework" name = "pyside2" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" version = "5.14.2.1" @@ -206,7 +206,7 @@ version = "0.2.0" category = "main" description = "Python / C++ bindings helper module" name = "shiboken2" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" version = "5.14.2.1" @@ -231,8 +231,11 @@ version = "3.1.0" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] +[extras] +qt = ["pyside2"] + [metadata] -content-hash = "e076be1de3ff88d71f7ed62865dc7f4ecc424ef5d409cc1b7db578021961164a" +content-hash = "bce2677f0c45cb74f03eecfab2094577eed8fb16fe305a21c924128c4faa1b47" python-versions = "^3.6,<3.9" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index 675db3021..588bc5e2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,10 @@ pyaes = "^1.6" mnemonic = "^0.18.0" typing-extensions = "^3.7" libusb1 = "^1.7" -pyside2 = "^5.14.0" +pyside2 = { version = "^5.14.0", optional = true } + +[tool.poetry.extras] +qt = ["pyside2"] [tool.poetry.dev-dependencies] pyinstaller = "^3.4" From 619b4122a4ff9d5253c1bbcdd88cf6857afac1fa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 15 May 2020 21:30:18 -0400 Subject: [PATCH 104/634] Update setup.py --- setup.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 40b373ab3..809fab731 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,89 @@ 'hwilib.devices.trezorlib.transport'] package_data = \ -{'': ['*'], 'hwilib': ['udev/*', 'ui/*']} +{'': ['*'], + 'hwilib': ['udev/*', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getkeypooloptionsdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/mainwindow.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/setpassphrasedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui', + 'ui/signpsbtdialog.ui']} modules = \ ['hwi', 'hwi-qt'] @@ -21,9 +103,11 @@ 'libusb1>=1.7,<2.0', 'mnemonic>=0.18.0,<0.19.0', 'pyaes>=1.6,<2.0', - 'pyside2>=5.14.0,<6.0.0', 'typing-extensions>=3.7,<4.0'] +extras_require = \ +{'qt': ['pyside2>=5.14.0,<6.0.0']} + entry_points = \ {'console_scripts': ['hwi = hwilib.cli:main', 'hwi-qt = hwilib.gui:main']} @@ -41,6 +125,7 @@ 'package_data': package_data, 'py_modules': modules, 'install_requires': install_requires, + 'extras_require': extras_require, 'entry_points': entry_points, 'python_requires': '>=3.6,<3.9', } From 7e65e2f6449593d664406dcd4e61a2a112bc4ef3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 24 May 2020 18:54:45 -0400 Subject: [PATCH 105/634] Use qt extra when generating releases --- contrib/build_bin.sh | 2 +- contrib/build_dist.sh | 2 +- contrib/build_wine.sh | 2 +- contrib/generate_setup.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index a502a36c4..8109da6d9 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -7,7 +7,7 @@ pip install -U pip pip install poetry # Setup poetry and install the dependencies -poetry install +poetry install -E qt # We now need to remove debugging symbols and build id from the hidapi SO file so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index b6b7d6b75..aba635f97 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -7,7 +7,7 @@ pip install -U pip pip install poetry # Setup poetry and install the dependencies -poetry install +poetry install -E qt # Make the distribution archives for pypi poetry build -f wheel diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 057a27d31..4d1a071d1 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -67,7 +67,7 @@ TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" # Install python dependencies POETRY="wine $PYHOME/Scripts/poetry.exe" sleep 5 # For some reason, pausing for a few seconds makes the next step work -$POETRY install +$POETRY install -E qt # make the ui files pushd hwilib/ui diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh index 5e944761a..098d9ea86 100755 --- a/contrib/generate_setup.sh +++ b/contrib/generate_setup.sh @@ -4,7 +4,7 @@ set -e # Setup poetry and install the dependencies -poetry install +poetry install -E qt # Build the source distribution poetry build -f sdist From edcee233c6b26b23331aaea3b719d75d585cae3d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 24 May 2020 19:06:53 -0400 Subject: [PATCH 106/634] fix generate-ui.sh for GNU sed This was changed to work with BSD sed (used by macOS) which breaks GNU sed. This should work for both. --- contrib/generate-ui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh index 980a87c24..fb17e34bf 100755 --- a/contrib/generate-ui.sh +++ b/contrib/generate-ui.sh @@ -5,6 +5,6 @@ for file in *.ui do gen_file=ui_`echo $file| cut -d. -f1`.py pyside2-uic $file -o $gen_file - sed -i '' -e 's/raise()/raise_()/g' $gen_file + sed -i'' -e 's/raise()/raise_()/g' $gen_file done popd From 3d43984fe8a7613776b3c1e097b9809a34db1289 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 24 May 2020 19:24:39 -0400 Subject: [PATCH 107/634] Use set -ex in all bash scripts --- contrib/build_bin.sh | 2 ++ contrib/build_dist.sh | 2 ++ contrib/build_wine.sh | 2 +- contrib/generate-ui.sh | 2 ++ contrib/generate_setup.sh | 2 +- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 8109da6d9..99e2082e2 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -1,6 +1,8 @@ #! /bin/bash # Script for building standalone binary releases deterministically +set -ex + eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index aba635f97..c8bd7fd36 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -1,6 +1,8 @@ #! /bin/bash # Script for building pypi distribution archives deterministically +set -ex + eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 4d1a071d1..7b48b204e 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -1,7 +1,7 @@ #!/bin/bash # Script which sets up Wine and builds the Windows standalone binary -set -e +set -ex PYTHON_VERSION=3.6.8 diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh index fb17e34bf..9fc982ea9 100755 --- a/contrib/generate-ui.sh +++ b/contrib/generate-ui.sh @@ -1,5 +1,7 @@ #! /bin/bash +set -ex + pushd hwilib/ui for file in *.ui do diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh index 098d9ea86..3e3424d8a 100755 --- a/contrib/generate_setup.sh +++ b/contrib/generate_setup.sh @@ -1,7 +1,7 @@ #! /bin/bash # Generates the setup.py file -set -e +set -ex # Setup poetry and install the dependencies poetry install -E qt From aaf40562e7df512ae3b8542c49e644943d6c5054 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 29 May 2020 21:59:08 -0400 Subject: [PATCH 108/634] release: bump to version 1.1.1-rc.1 and regenerate setup.py --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index 1a72d32e5..8bc69c9aa 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.1.0' +__version__ = '1.1.1-rc.1' diff --git a/pyproject.toml b/pyproject.toml index 588bc5e2b..0e38b6567 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.0" +version = "1.1.1-rc.1" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index 809fab731..847876949 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.0', + 'version': '1.1.1rc1', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', From 25e6d075247b168f9cc6b3ddfa6ab071837554d9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 1 Jun 2020 22:01:39 -0400 Subject: [PATCH 109/634] release: bump to version 1.1.1 and regenerate setup.py --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index 8bc69c9aa..b3ddbc41f 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.1.1-rc.1' +__version__ = '1.1.1' diff --git a/pyproject.toml b/pyproject.toml index 0e38b6567..3b1199496 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.1-rc.1" +version = "1.1.1" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index 847876949..ef04ca6ea 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.1rc1', + 'version': '1.1.1', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', From 65c6de6b8a625eb272d1c0802473095093164821 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 19:59:19 -0400 Subject: [PATCH 110/634] psbt: Allow PSBTs to have both non_witness_utxo and witness_utxo --- hwilib/serializations.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 58d1f4b1a..7dcf58af9 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -582,7 +582,7 @@ def serialize(self): tx = self.non_witness_utxo.serialize_with_witness() r += ser_string(tx) - elif self.witness_utxo: + if self.witness_utxo: r += ser_string(b"\x01") tx = self.witness_utxo.serialize() r += ser_string(tx) @@ -623,19 +623,6 @@ def serialize(self): return r - def is_sane(self): - # Cannot have both witness and non-witness utxos - if self.witness_utxo and self.non_witness_utxo: - return False - - # if we have witness script or scriptwitness, must have witness utxo - if len(self.witness_script) != 0 and not self.witness_utxo: - return False - if not self.final_script_witness.is_null() and not self.witness_utxo: - return False - - return True - class PartiallySignedOutput: def __init__(self): self.redeem_script = b"" @@ -795,9 +782,6 @@ def deserialize(self, psbt): if len(self.outputs) != len(self.tx.vout): raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") - if not self.is_sane(): - raise PSBTSerializationError("PSBT is not sane") - def serialize(self): r = b"" @@ -831,12 +815,6 @@ def serialize(self): # return hex string return HexToBase64(binascii.hexlify(r)).decode() - def is_sane(self): - for input in self.inputs: - if not input.is_sane(): - return False - return True - # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): From 3b6131c84ae8f9d3a9a71170130e8baef25293bb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 20:06:19 -0400 Subject: [PATCH 111/634] test: Patch bitcoind to give out PSBTs with both UTXOs --- test/data/psbt_non_witness_utxo_segwit.patch | 431 +++++++++++++++++++ test/setup_environment.sh | 1 + 2 files changed, 432 insertions(+) create mode 100644 test/data/psbt_non_witness_utxo_segwit.patch diff --git a/test/data/psbt_non_witness_utxo_segwit.patch b/test/data/psbt_non_witness_utxo_segwit.patch new file mode 100644 index 000000000..49b1f7967 --- /dev/null +++ b/test/data/psbt_non_witness_utxo_segwit.patch @@ -0,0 +1,431 @@ +From 30477b10795dc3e819cc852823f2e00e4e23f770 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Thu, 4 Jun 2020 23:43:25 -0400 +Subject: [PATCH 1/3] rpc: show both UTXOs in decodepsbt + +--- + src/rpc/rawtransaction.cpp | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp +index e14217c307..45cf6be3a0 100644 +--- a/src/rpc/rawtransaction.cpp ++++ b/src/rpc/rawtransaction.cpp +@@ -1104,6 +1104,7 @@ UniValue decodepsbt(const JSONRPCRequest& request) + const PSBTInput& input = psbtx.inputs[i]; + UniValue in(UniValue::VOBJ); + // UTXOs ++ bool have_a_utxo = false; + if (!input.witness_utxo.IsNull()) { + const CTxOut& txout = input.witness_utxo; + +@@ -1121,7 +1122,9 @@ UniValue decodepsbt(const JSONRPCRequest& request) + ScriptToUniv(txout.scriptPubKey, o, true); + out.pushKV("scriptPubKey", o); + in.pushKV("witness_utxo", out); +- } else if (input.non_witness_utxo) { ++ have_a_utxo = true; ++ } ++ if (input.non_witness_utxo) { + UniValue non_wit(UniValue::VOBJ); + TxToUniv(*input.non_witness_utxo, uint256(), non_wit, false); + in.pushKV("non_witness_utxo", non_wit); +@@ -1132,7 +1135,9 @@ UniValue decodepsbt(const JSONRPCRequest& request) + // Hack to just not show fee later + have_all_utxos = false; + } +- } else { ++ have_a_utxo = true; ++ } ++ if (!have_a_utxo) { + have_all_utxos = false; + } + +-- +2.27.0 + + +From fcf1ef6529a010d05a79f9ba6dbfbc480de99af5 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Thu, 4 Jun 2020 23:43:39 -0400 +Subject: [PATCH 2/3] psbt: Allow both non_witness_utxo and witness_utxo + +--- + src/psbt.cpp | 29 ----------------------------- + src/psbt.h | 7 ------- + src/test/fuzz/psbt.cpp | 2 -- + src/wallet/scriptpubkeyman.cpp | 10 ---------- + src/wallet/wallet.cpp | 5 ----- + 5 files changed, 53 deletions(-) + +diff --git a/src/psbt.cpp b/src/psbt.cpp +index ef9781817a..4c8b40ca0b 100644 +--- a/src/psbt.cpp ++++ b/src/psbt.cpp +@@ -35,14 +35,6 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt) + return true; + } + +-bool PartiallySignedTransaction::IsSane() const +-{ +- for (PSBTInput input : inputs) { +- if (!input.IsSane()) return false; +- } +- return true; +-} +- + bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin) + { + if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) { +@@ -158,18 +150,6 @@ void PSBTInput::Merge(const PSBTInput& input) + if (final_script_witness.IsNull() && !input.final_script_witness.IsNull()) final_script_witness = input.final_script_witness; + } + +-bool PSBTInput::IsSane() const +-{ +- // Cannot have both witness and non-witness utxos +- if (!witness_utxo.IsNull() && non_witness_utxo) return false; +- +- // If we have a witness_script or a scriptWitness, we must also have a witness utxo +- if (!witness_script.empty() && witness_utxo.IsNull()) return false; +- if (!final_script_witness.IsNull() && witness_utxo.IsNull()) return false; +- +- return true; +-} +- + void PSBTOutput::FillSignatureData(SignatureData& sigdata) const + { + if (!redeem_script.empty()) { +@@ -250,11 +230,6 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& + bool require_witness_sig = false; + CTxOut utxo; + +- // Verify input sanity, which checks that at most one of witness or non-witness utxos is provided. +- if (!input.IsSane()) { +- return false; +- } +- + if (input.non_witness_utxo) { + // If we're taking our information from a non-witness UTXO, verify that it matches the prevout. + COutPoint prevout = tx.vin[index].prevout; +@@ -345,10 +320,6 @@ TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector + return TransactionError::PSBT_MISMATCH; + } + } +- if (!out.IsSane()) { +- return TransactionError::INVALID_PSBT; +- } +- + return TransactionError::OK; + } + +diff --git a/src/psbt.h b/src/psbt.h +index 888e0fd119..cbf4296bd2 100644 +--- a/src/psbt.h ++++ b/src/psbt.h +@@ -62,7 +62,6 @@ struct PSBTInput + void FillSignatureData(SignatureData& sigdata) const; + void FromSignatureData(const SignatureData& sigdata); + void Merge(const PSBTInput& input); +- bool IsSane() const; + PSBTInput() {} + + template +@@ -284,7 +283,6 @@ struct PSBTOutput + void FillSignatureData(SignatureData& sigdata) const; + void FromSignatureData(const SignatureData& sigdata); + void Merge(const PSBTOutput& output); +- bool IsSane() const; + PSBTOutput() {} + + template +@@ -401,7 +399,6 @@ struct PartiallySignedTransaction + /** Merge psbt into this. The two psbts must have the same underlying CTransaction (i.e. the + * same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */ + NODISCARD bool Merge(const PartiallySignedTransaction& psbt); +- bool IsSane() const; + bool AddInput(const CTxIn& txin, PSBTInput& psbtin); + bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout); + PartiallySignedTransaction() {} +@@ -551,10 +548,6 @@ struct PartiallySignedTransaction + if (outputs.size() != tx->vout.size()) { + throw std::ios_base::failure("Outputs provided does not match the number of outputs in transaction."); + } +- // Sanity check +- if (!IsSane()) { +- throw std::ios_base::failure("PSBT is not sane."); +- } + } + + template +diff --git a/src/test/fuzz/psbt.cpp b/src/test/fuzz/psbt.cpp +index 64328fb66e..908e2b16f2 100644 +--- a/src/test/fuzz/psbt.cpp ++++ b/src/test/fuzz/psbt.cpp +@@ -39,7 +39,6 @@ void test_one_input(const std::vector& buffer) + } + + (void)psbt.IsNull(); +- (void)psbt.IsSane(); + + Optional tx = psbt.tx; + if (tx) { +@@ -50,7 +49,6 @@ void test_one_input(const std::vector& buffer) + for (const PSBTInput& input : psbt.inputs) { + (void)PSBTInputSigned(input); + (void)input.IsNull(); +- (void)input.IsSane(); + } + + for (const PSBTOutput& output : psbt.outputs) { +diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp +index 8a2a798644..9fae27975d 100644 +--- a/src/wallet/scriptpubkeyman.cpp ++++ b/src/wallet/scriptpubkeyman.cpp +@@ -595,11 +595,6 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb + continue; + } + +- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. +- if (!input.IsSane()) { +- return TransactionError::INVALID_PSBT; +- } +- + // Get the Sighash type + if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { + return TransactionError::SIGHASH_MISMATCH; +@@ -2074,11 +2069,6 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& + continue; + } + +- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. +- if (!input.IsSane()) { +- return TransactionError::INVALID_PSBT; +- } +- + // Get the Sighash type + if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { + return TransactionError::SIGHASH_MISMATCH; +diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp +index 89737ca7b5..054b312cd7 100644 +--- a/src/wallet/wallet.cpp ++++ b/src/wallet/wallet.cpp +@@ -2490,11 +2490,6 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp + continue; + } + +- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. +- if (!input.IsSane()) { +- return TransactionError::INVALID_PSBT; +- } +- + // If we have no utxo, grab it from the wallet. + if (!input.non_witness_utxo && input.witness_utxo.IsNull()) { + const uint256& txhash = txin.prevout.hash; +-- +2.27.0 + + +From 946b5c010fa23103c1ee5f584bd5be240e42c4c9 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Thu, 4 Jun 2020 23:43:43 -0400 +Subject: [PATCH 3/3] psbt: always put a non_witness_utxo and don't remove it + +Offline signers will always need a non_witness_utxo so make sure it is +there. +--- + src/psbt.cpp | 7 +- + src/psbt.h | 4 +- + src/wallet/test/psbt_wallet_tests.cpp | 2 +- + src/wallet/wallet.cpp | 2 +- + test/functional/rpc_psbt.py | 94 ++++++++++++++------------- + 5 files changed, 56 insertions(+), 53 deletions(-) + +diff --git a/src/psbt.cpp b/src/psbt.cpp +index 4c8b40ca0b..51f829d533 100644 +--- a/src/psbt.cpp ++++ b/src/psbt.cpp +@@ -136,8 +136,8 @@ void PSBTInput::Merge(const PSBTInput& input) + { + if (!non_witness_utxo && input.non_witness_utxo) non_witness_utxo = input.non_witness_utxo; + if (witness_utxo.IsNull() && !input.witness_utxo.IsNull()) { ++ // TODO: For segwit v1, we will want to clear out the non-witness utxo when setting a witness one. For v0 and non-segwit, this is not safe + witness_utxo = input.witness_utxo; +- non_witness_utxo = nullptr; // Clear out any non-witness utxo when we set a witness one. + } + + partial_sigs.insert(input.partial_sigs.begin(), input.partial_sigs.end()); +@@ -263,10 +263,11 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& + if (require_witness_sig && !sigdata.witness) return false; + input.FromSignatureData(sigdata); + +- // If we have a witness signature, use the smaller witness UTXO. ++ // If we have a witness signature, put a witness UTXO. ++ // TODO: For segwit v1, we should remove the non_witness_utxo + if (sigdata.witness) { + input.witness_utxo = utxo; +- input.non_witness_utxo = nullptr; ++ // input.non_witness_utxo = nullptr; + } + + // Fill in the missing info +diff --git a/src/psbt.h b/src/psbt.h +index cbf4296bd2..275fb03cd8 100644 +--- a/src/psbt.h ++++ b/src/psbt.h +@@ -67,12 +67,12 @@ struct PSBTInput + template + inline void Serialize(Stream& s) const { + // Write the utxo +- // If there is a non-witness utxo, then don't add the witness one. + if (non_witness_utxo) { + SerializeToVector(s, PSBT_IN_NON_WITNESS_UTXO); + OverrideStream os(&s, s.GetType(), s.GetVersion() | SERIALIZE_TRANSACTION_NO_WITNESS); + SerializeToVector(os, non_witness_utxo); +- } else if (!witness_utxo.IsNull()) { ++ } ++ if (!witness_utxo.IsNull()) { + SerializeToVector(s, PSBT_IN_WITNESS_UTXO); + SerializeToVector(s, witness_utxo); + } +diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp +index b4c65a8665..119457aadf 100644 +--- a/src/wallet/test/psbt_wallet_tests.cpp ++++ b/src/wallet/test/psbt_wallet_tests.cpp +@@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << psbtx; + std::string final_hex = HexStr(ssTx.begin(), ssTx.end()); +- BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); ++ BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001008a020000000158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8876500000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); + + // Mutate the transaction so that one of the inputs is invalid + psbtx.tx->vin[0].prevout.n = 2; +diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp +index 054b312cd7..7816a39ec1 100644 +--- a/src/wallet/wallet.cpp ++++ b/src/wallet/wallet.cpp +@@ -2491,7 +2491,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp + } + + // If we have no utxo, grab it from the wallet. +- if (!input.non_witness_utxo && input.witness_utxo.IsNull()) { ++ if (!input.non_witness_utxo) { + const uint256& txhash = txin.prevout.hash; + const auto it = mapWallet.find(txhash); + if (it != mapWallet.end()) { +diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py +index 51d136d26a..735293bbc7 100755 +--- a/test/functional/rpc_psbt.py ++++ b/test/functional/rpc_psbt.py +@@ -37,51 +37,52 @@ class PSBTTest(BitcoinTestFramework): + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + +- def test_utxo_conversion(self): +- mining_node = self.nodes[2] +- offline_node = self.nodes[0] +- online_node = self.nodes[1] +- +- # Disconnect offline node from others +- disconnect_nodes(offline_node, 1) +- disconnect_nodes(online_node, 0) +- disconnect_nodes(offline_node, 2) +- disconnect_nodes(mining_node, 0) +- +- # Create watchonly on online_node +- online_node.createwallet(wallet_name='wonline', disable_private_keys=True) +- wonline = online_node.get_wallet_rpc('wonline') +- w2 = online_node.get_wallet_rpc('') +- +- # Mine a transaction that credits the offline address +- offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit") +- online_addr = w2.getnewaddress(address_type="p2sh-segwit") +- wonline.importaddress(offline_addr, "", False) +- mining_node.sendtoaddress(address=offline_addr, amount=1.0) +- mining_node.generate(nblocks=1) +- self.sync_blocks([mining_node, online_node]) +- +- # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) +- utxos = wonline.listunspent(addresses=[offline_addr]) +- raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}]) +- psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"] +- assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] +- +- # Have the offline node sign the PSBT (which will update the UTXO to segwit) +- signed_psbt = offline_node.walletprocesspsbt(psbt)["psbt"] +- assert "witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] +- +- # Make sure we can mine the resulting transaction +- txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) +- mining_node.generate(1) +- self.sync_blocks([mining_node, online_node]) +- assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) +- +- wonline.unloadwallet() +- +- # Reconnect +- connect_nodes(self.nodes[0], 1) +- connect_nodes(self.nodes[0], 2) ++ # TODO: Re-enable this test with segwit v1 ++ # def test_utxo_conversion(self): ++ # mining_node = self.nodes[2] ++ # offline_node = self.nodes[0] ++ # online_node = self.nodes[1] ++ # ++ # # Disconnect offline node from others ++ # disconnect_nodes(offline_node, 1) ++ # disconnect_nodes(online_node, 0) ++ # disconnect_nodes(offline_node, 2) ++ # disconnect_nodes(mining_node, 0) ++ # ++ # # Create watchonly on online_node ++ # online_node.createwallet(wallet_name='wonline', disable_private_keys=True) ++ # wonline = online_node.get_wallet_rpc('wonline') ++ # w2 = online_node.get_wallet_rpc('') ++ # ++ # # Mine a transaction that credits the offline address ++ # offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit") ++ # online_addr = w2.getnewaddress(address_type="p2sh-segwit") ++ # wonline.importaddress(offline_addr, "", False) ++ # mining_node.sendtoaddress(address=offline_addr, amount=1.0) ++ # mining_node.generate(nblocks=1) ++ # self.sync_blocks([mining_node, online_node]) ++ # ++ # # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) ++ # utxos = wonline.listunspent(addresses=[offline_addr]) ++ # raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}]) ++ # psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"] ++ # assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] ++ # ++ # # Have the offline node sign the PSBT (which will update the UTXO to segwit) ++ # signed_psbt = offline_node.walletprocesspsbt(psbt)["psbt"] ++ # assert "witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] ++ # ++ # # Make sure we can mine the resulting transaction ++ # txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) ++ # mining_node.generate(1) ++ # self.sync_blocks([mining_node, online_node]) ++ # assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) ++ # ++ # wonline.unloadwallet() ++ # ++ # # Reconnect ++ # connect_nodes(self.nodes[0], 1) ++ # connect_nodes(self.nodes[0], 2) + + def run_test(self): + # Create and fund a raw tx for sending 10 BTC +@@ -346,7 +347,8 @@ class PSBTTest(BitcoinTestFramework): + for i, signer in enumerate(signers): + self.nodes[2].unloadwallet("wallet{}".format(i)) + +- self.test_utxo_conversion() ++ # TODO: Re-enable this for segwit v1 ++ # self.test_utxo_conversion() + + # Test that psbts with p2pkh outputs are created properly + p2pkh = self.nodes[0].getnewaddress(address_type='legacy') +-- +2.27.0 + diff --git a/test/setup_environment.sh b/test/setup_environment.sh index acb9af26f..6bf0c525a 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -216,6 +216,7 @@ else bitcoind_setup_needed=true fi fi +git am ../../data/psbt_non_witness_utxo_segwit.patch # Build bitcoind. This is super slow, but it is cached so it runs fairly quickly. if [ "$bitcoind_setup_needed" == true ] ; then From d662fad5b7803606dc05ad15269c65e3890d41ac Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 21:46:46 -0400 Subject: [PATCH 112/634] serializations: Refactor script type matching into separate functions --- hwilib/serializations.py | 53 ++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 7dcf58af9..968088aa4 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -242,6 +242,42 @@ def __repr__(self): self.nSequence) +def is_p2sh(script): + return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 + +def is_p2pkh(script): + return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac + +def is_p2pk(script): + return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac + +def is_witness(script): + if len(script) < 4 or len(script) > 42: + return (False, None, None) + + if script[0] != 0 and (script[0] < 81 or script[0] > 96): + return (False, None, None) + + if script[1] + 2 == len(script): + return (True, script[0] - 0x50 if script[0] else 0, script[2:]) + +def is_p2wpkh(script): + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 20 + +def is_p2wsh(script): + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 32 + + class CTxOut(object): def __init__(self, nValue=0, scriptPubKey=b""): self.nValue = nValue @@ -258,25 +294,16 @@ def serialize(self): return r def is_p2sh(self): - return len(self.scriptPubKey) == 23 and self.scriptPubKey[0] == 0xa9 and self.scriptPubKey[1] == 0x14 and self.scriptPubKey[22] == 0x87 + return is_p2sh(self.scriptPubKey) def is_p2pkh(self): - return len(self.scriptPubKey) == 25 and self.scriptPubKey[0] == 0x76 and self.scriptPubKey[1] == 0xa9 and self.scriptPubKey[2] == 0x14 and self.scriptPubKey[23] == 0x88 and self.scriptPubKey[24] == 0xac + return is_p2pkh(self.scriptPubKey) def is_p2pk(self): - return (len(self.scriptPubKey) == 35 or len(self.scriptPubKey) == 67) and (self.scriptPubKey[0] == 0x21 or self.scriptPubKey[0] == 0x41) and self.scriptPubKey[-1] == 0xac + return is_p2pk(self.scriptPubKey) def is_witness(self): - if len(self.scriptPubKey) < 4 or len(self.scriptPubKey) > 42: - return (False, None, None) - - if self.scriptPubKey[0] != 0 and (self.scriptPubKey[0] < 81 or self.scriptPubKey[0] > 96): - return (False, None, None) - - if self.scriptPubKey[1] + 2 == len(self.scriptPubKey): - return (True, self.scriptPubKey[0] - 0x50 if self.scriptPubKey[0] else 0, self.scriptPubKey[2:]) - - return (False, None, None) + return is_witness(self.scriptPubKey) def __repr__(self): return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ From f862135519f259b72436068f5997d025fb847a15 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 14:48:23 -0400 Subject: [PATCH 113/634] trezor: Set coin name as testnet when --testnet Also update displayaddress test to use --testnet --- hwilib/devices/trezor.py | 20 ++++++++++---------- test/test_device.py | 5 +++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 383006015..a8fc85eb7 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -109,6 +109,7 @@ def __init__(self, path, password='', expert=False): self.type = 'Trezor' def _check_unlocked(self): + self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' self.client.init_device() if self.client.features.model == 'T': self.client.ui.disallow_passphrase() @@ -124,7 +125,7 @@ def get_pubkey_at_path(self, path): expanded_path = tools.parse_path(path) except ValueError as e: raise BadArgumentError(str(e)) - output = btc.get_public_node(self.client, expanded_path) + output = btc.get_public_node(self.client, expanded_path, coin_name=self.coin_name) if self.is_testnet: result = {'xpub': xpub_main_2_test(output.xpub)} else: @@ -142,7 +143,7 @@ def sign_tx(self, tx): self._check_unlocked() # Get this devices master key fingerprint - master_key = btc.get_public_node(self.client, [0]) + master_key = btc.get_public_node(self.client, [0x80000000], coin_name='Bitcoin') master_fp = get_xpub_fingerprint(master_key.xpub) # Do multiple passes for multisig @@ -321,10 +322,7 @@ def ignore_input(): tx_details = proto.SignTx() tx_details.version = tx.tx.nVersion tx_details.lock_time = tx.tx.nLockTime - if self.is_testnet: - signed_tx = btc.sign_tx(self.client, "Testnet", inputs, outputs, tx_details, prevtxs) - else: - signed_tx = btc.sign_tx(self.client, "Bitcoin", inputs, outputs, tx_details, prevtxs) + signed_tx = btc.sign_tx(self.client, self.coin_name, inputs, outputs, tx_details, prevtxs) # Each input has one signature for input_num, (psbt_in, sig) in py_enumerate(list(zip(tx.inputs, signed_tx[0]))): @@ -346,7 +344,7 @@ def ignore_input(): def sign_message(self, message, keypath): self._check_unlocked() path = tools.parse_path(keypath) - result = btc.sign_message(self.client, 'Bitcoin', path, message) + result = btc.sign_message(self.client, self.coin_name, path, message) return {'signature': base64.b64encode(result.signature).decode('utf-8')} # Display address of specified type on the device. Only supports single-key based addresses. @@ -354,12 +352,13 @@ def sign_message(self, message, keypath): def display_address(self, keypath, p2sh_p2wpkh, bech32): self._check_unlocked() expanded_path = tools.parse_path(keypath) + script_type = proto.InputScriptType.SPENDWITNESS if bech32 else (proto.InputScriptType.SPENDP2SHWITNESS if p2sh_p2wpkh else proto.InputScriptType.SPENDADDRESS) address = btc.get_address( self.client, - "Testnet" if self.is_testnet else "Bitcoin", + self.coin_name, expanded_path, show_display=True, - script_type=proto.InputScriptType.SPENDWITNESS if bech32 else (proto.InputScriptType.SPENDP2SHWITNESS if p2sh_p2wpkh else proto.InputScriptType.SPENDADDRESS) + script_type=script_type ) return {'address': address} @@ -406,6 +405,7 @@ def close(self): # Prompt for a pin on device @trezor_exception def prompt_pin(self): + self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' self.client.open() self.client.init_device() if not self.client.features.pin_protection: @@ -414,7 +414,7 @@ def prompt_pin(self): raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - self.client.call_raw(proto.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=None, script_type=proto.InputScriptType.SPENDADDRESS)) + self.client.call_raw(proto.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=proto.InputScriptType.SPENDADDRESS)) return {'success': True} # Send the pin diff --git a/test/test_device.py b/test/test_device.py index 068a6699d..b1affe738 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -456,6 +456,11 @@ def test_big_tx(self): pass class TestDisplayAddress(DeviceTestCase): + def setUp(self): + if '--testnet' not in self.dev_args: + self.dev_args.append('--testnet') + self.emulator.start() + def test_display_address_bad_args(self): result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) self.assertIn('error', result) From 2f40d0f2f90557204c7d4a4de4e702e6290ca88b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 16:01:51 -0400 Subject: [PATCH 114/634] trezor: Use standard derivation paths --- hwilib/devices/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index a8fc85eb7..f728fd683 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -186,7 +186,7 @@ def sign_tx(self, tx): scriptcode = psbt_in.redeem_script def ignore_input(): - txinputtype.address_n = [0x80000000] + txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0)] txinputtype.multisig = None txinputtype.script_type = proto.InputScriptType.SPENDWITNESS inputs.append(txinputtype) From 4ba72f36aae1a00d6619c68cf6ea24da88a41fa5 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 16:02:21 -0400 Subject: [PATCH 115/634] trezor: handle when psbt has both types of UTXOs --- hwilib/devices/trezor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index f728fd683..aaa27df95 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -164,12 +164,7 @@ def sign_tx(self, tx): # Detrermine spend type scriptcode = b'' - if psbt_in.non_witness_utxo: - utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] - txinputtype.script_type = proto.InputScriptType.SPENDADDRESS - scriptcode = utxo.scriptPubKey - txinputtype.amount = psbt_in.non_witness_utxo.vout[txin.prevout.n].nValue - elif psbt_in.witness_utxo: + if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo # Check if the output is p2sh if psbt_in.witness_utxo.is_p2sh(): @@ -178,6 +173,11 @@ def sign_tx(self, tx): txinputtype.script_type = proto.InputScriptType.SPENDWITNESS scriptcode = psbt_in.witness_utxo.scriptPubKey txinputtype.amount = psbt_in.witness_utxo.nValue + elif psbt_in.non_witness_utxo: + utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] + txinputtype.script_type = proto.InputScriptType.SPENDADDRESS + scriptcode = utxo.scriptPubKey + txinputtype.amount = psbt_in.non_witness_utxo.vout[txin.prevout.n].nValue # Set the script if psbt_in.witness_script: @@ -197,7 +197,7 @@ def ignore_input(): if is_ms: # Add to txinputtype txinputtype.multisig = multisig - if psbt_in.non_witness_utxo: + if not psbt_in.witness_utxo: if utxo.is_p2sh: txinputtype.script_type = proto.InputScriptType.SPENDMULTISIG else: From a264d844223aeff44985f69f29de90198e389154 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 18:13:16 -0400 Subject: [PATCH 116/634] test: Do legacy, then segwit, then mixed --- test/test_device.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index b1affe738..ca283a155 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -420,10 +420,9 @@ def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'} supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'} - if self.full_type not in supports_mixed: - self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external) - self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external) - else: + self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external) + self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external) + if self.full_type in supports_mixed: self._test_signtx("all", self.full_type in supports_multisig, self.full_type in supports_external) # Make a huge transaction which might cause some problems with different interfaces From 9f8e03c85b29a69d4b676047da0f0559f1222cc0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 19:58:07 -0400 Subject: [PATCH 117/634] trezorlib: Update sign_tx from upstream --- hwilib/devices/trezorlib/btc.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index f9c56cf0a..3104693f7 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -71,22 +71,7 @@ def sign_message( @session def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None): - # set up a transactions dict - txes = {None: messages.TransactionType(inputs=inputs, outputs=outputs)} - # preload all relevant transactions ahead of time - for inp in inputs: - if inp.script_type not in ( - messages.InputScriptType.SPENDP2SHWITNESS, - messages.InputScriptType.SPENDWITNESS, - messages.InputScriptType.EXTERNAL, - ): - try: - prev_tx = prev_txes[inp.prev_hash] - except Exception as e: - raise ValueError("Could not retrieve prev_tx") from e - if not isinstance(prev_tx, messages.TransactionType): - raise ValueError("Invalid value for prev_tx") from None - txes[inp.prev_hash] = prev_tx + this_tx = messages.TransactionType(inputs=inputs, outputs=outputs) if details is None: signtx = messages.SignTx() @@ -104,8 +89,7 @@ def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None): serialized_tx = b"" def copy_tx_meta(tx): - tx_copy = messages.TransactionType() - tx_copy.CopyFrom(tx) + tx_copy = messages.TransactionType(**tx) # clear fields tx_copy.inputs_cnt = len(tx.inputs) tx_copy.inputs = [] @@ -134,7 +118,10 @@ def copy_tx_meta(tx): break # Device asked for one more information, let's process it. - current_tx = txes[res.details.tx_hash] + if res.details.tx_hash is not None: + current_tx = prev_txes[res.details.tx_hash] + else: + current_tx = this_tx if res.request_type == R.TXMETA: msg = copy_tx_meta(current_tx) @@ -160,13 +147,10 @@ def copy_tx_meta(tx): msg.extra_data = current_tx.extra_data[o : o + l] res = client.call(messages.TxAck(tx=msg)) - if isinstance(res, messages.Failure): - raise CallException("Signing failed") - if not isinstance(res, messages.TxRequest): - raise CallException("Unexpected message") + raise exceptions.TrezorException("Unexpected message") if None in signatures: - raise RuntimeError("Some signatures are missing!") + raise exceptions.TrezorException("Some signatures are missing!") return signatures, serialized_tx From 249825d4d5176fc420426b4874b6bf8d38edefc5 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 21:58:50 -0400 Subject: [PATCH 118/634] Make imports multiline Just make this easier to read --- hwilib/cli.py | 25 ++++++++++++++--- hwilib/commands.py | 6 ++++- hwilib/devices/coldcard.py | 40 ++++++++++++++++++++++----- hwilib/devices/digitalbitbox.py | 28 ++++++++++++++++--- hwilib/devices/keepkey.py | 12 +++++++-- hwilib/devices/ledger.py | 22 ++++++++++++--- hwilib/devices/trezor.py | 48 ++++++++++++++++++++++++++++----- 7 files changed, 155 insertions(+), 26 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index be8d19863..6d83d10ce 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -1,15 +1,32 @@ #! /usr/bin/env python3 -from .commands import backup_device, displayaddress, enumerate, find_device, \ - get_client, getmasterxpub, getxpub, getkeypool, getdescriptors, prompt_pin, toggle_passphrase, restore_device, send_pin, setup_device, \ - signmessage, signtx, wipe_device, install_udev_rules +from .commands import ( + backup_device, + displayaddress, + enumerate, + find_device, + get_client, + getmasterxpub, + getxpub, + getkeypool, + getdescriptors, + prompt_pin, + toggle_passphrase, + restore_device, + send_pin, + setup_device, + signmessage, + signtx, + wipe_device, + install_udev_rules, +) from .errors import ( handle_errors, DEVICE_CONN_ERROR, HELP_TEXT, MISSING_ARGUMENTS, NO_DEVICE_TYPE, - UNAVAILABLE_ACTION + UNAVAILABLE_ACTION, ) from . import __version__ diff --git a/hwilib/commands.py b/hwilib/commands.py index b18f8e443..df59df1c2 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -7,7 +7,11 @@ from .serializations import PSBT from .base58 import xpub_to_pub_hex -from .errors import UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED +from .errors import ( + UnknownDeviceError, + BAD_ARGUMENT, + NOT_IMPLEMENTED, +) from .descriptor import Descriptor from .devices import __all__ as all_devs from enum import Enum diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index fc11b7878..2c21c3bad 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -2,12 +2,40 @@ from binascii import b2a_hex from ..hwwclient import HardwareWalletClient -from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, DeviceFailureError, UnavailableActionError, common_err_msgs, handle_errors -from .ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID -from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused -from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH -from ..base58 import get_xpub_fingerprint, xpub_main_2_test -from ..serializations import ExtendedKey, PSBT +from ..errors import ( + ActionCanceledError, + BadArgumentError, + DeviceBusyError, + DeviceFailureError, + UnavailableActionError, + common_err_msgs, + handle_errors, +) +from .ckcc.client import ( + ColdcardDevice, + COINKITE_VID, + CKCC_PID, +) +from .ckcc.protocol import ( + CCProtocolPacker, + CCBusyError, + CCProtoError, + CCUserRefused, +) +from .ckcc.constants import ( + MAX_BLK_LEN, + AF_P2WPKH, + AF_CLASSIC, + AF_P2WPKH_P2SH, +) +from ..base58 import ( + get_xpub_fingerprint, + xpub_main_2_test, +) +from ..serializations import ( + ExtendedKey, + PSBT, +) from hashlib import sha256 import base64 diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 2af7b23b3..301770813 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -15,10 +15,30 @@ import time from ..hwwclient import HardwareWalletClient -from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors -from ..serializations import CTransaction, ExtendedKey, hash256, ser_sig_der, ser_sig_compact, ser_compact_size -from ..base58 import get_xpub_fingerprint, xpub_main_2_test - +from ..errors import ( + ActionCanceledError, + BadArgumentError, + DeviceFailureError, + DeviceAlreadyInitError, + DEVICE_NOT_INITIALIZED, + DeviceNotReadyError, + NoPasswordError, + UnavailableActionError, + common_err_msgs, + handle_errors, +) +from ..serializations import ( + CTransaction, + ExtendedKey, + hash256, + ser_sig_der, + ser_sig_compact, + ser_compact_size, +) +from ..base58 import ( + get_xpub_fingerprint, + xpub_main_2_test, +) applen = 225280 # flash size minus bootloader length chunksize = 8 * 512 usb_report_size = 64 # firmware > v2.0 diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index e103477b6..ab69ef6fc 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -1,7 +1,15 @@ # KeepKey interaction script -from ..errors import DEVICE_NOT_INITIALIZED, DeviceNotReadyError, common_err_msgs, handle_errors -from .trezorlib.transport import enumerate_devices, KEEPKEY_VENDOR_IDS +from ..errors import ( + DEVICE_NOT_INITIALIZED, + DeviceNotReadyError, + common_err_msgs, + handle_errors, +) +from .trezorlib.transport import ( + enumerate_devices, + KEEPKEY_VENDOR_IDS, +) from .trezor import TrezorClient py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index f12f373a4..ced716dbf 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,17 +1,33 @@ # Ledger interaction script from ..hwwclient import HardwareWalletClient -from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError, common_err_msgs, handle_errors +from ..errors import ( + ActionCanceledError, + BadArgumentError, + DeviceConnectionError, + DeviceFailureError, + UnavailableActionError, + common_err_msgs, + handle_errors, +) from .btchip.bitcoinTransaction import bitcoinTransaction from .btchip.btchip import btchip -from .btchip.btchipComm import DongleServer, HIDDongleHIDAPI +from .btchip.btchipComm import ( + DongleServer, + HIDDongleHIDAPI, +) from .btchip.btchipException import BTChipException from .btchip.btchipUtils import compress_public_key import base64 import hid import struct from .. import base58 -from ..serializations import ExtendedKey, hash256, hash160, CTransaction +from ..serializations import ( + ExtendedKey, + hash256, + hash160, + CTransaction, +) import logging import re diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index aaa27df95..979ea9570 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,16 +1,52 @@ # Trezor interaction script from ..hwwclient import HardwareWalletClient -from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, UnavailableActionError, common_err_msgs, handle_errors +from ..errors import ( + ActionCanceledError, + BadArgumentError, + DeviceAlreadyInitError, + DeviceAlreadyUnlockedError, + DeviceConnectionError, + DEVICE_NOT_INITIALIZED, + DeviceNotReadyError, + UnavailableActionError, + common_err_msgs, + handle_errors, +) from .trezorlib.client import TrezorClient as Trezor from .trezorlib.debuglink import TrezorClientDebugLink from .trezorlib.exceptions import Cancelled -from .trezorlib.transport import enumerate_devices, get_transport, TREZOR_VENDOR_IDS -from .trezorlib.ui import echo, PassphraseUI, mnemonic_words, PIN_CURRENT, PIN_NEW, PIN_CONFIRM, PIN_MATRIX_DESCRIPTION, prompt -from .trezorlib import tools, btc, device +from .trezorlib.transport import ( + enumerate_devices, + get_transport, + TREZOR_VENDOR_IDS, +) +from .trezorlib.ui import ( + echo, + PassphraseUI, + mnemonic_words, + PIN_CURRENT, + PIN_NEW, + PIN_CONFIRM, + PIN_MATRIX_DESCRIPTION, + prompt, +) +from .trezorlib import ( + tools, + btc, + device, +) from .trezorlib import messages as proto -from ..base58 import get_xpub_fingerprint, to_address, xpub_main_2_test -from ..serializations import CTxOut, ExtendedKey, ser_uint256 +from ..base58 import ( + get_xpub_fingerprint, + to_address, + xpub_main_2_test, +) +from ..serializations import ( + CTxOut, + ExtendedKey, + ser_uint256, +) from .. import bech32 from usb1 import USBErrorNoDevice from types import MethodType From 662eab0c4bfd4e181ce5f1a1465e61277cc108e3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:03:37 -0400 Subject: [PATCH 119/634] bitbox: Update to inspect scriptcode for signature type Rather than relying on the utxos given to determine the signature type, inspect the scripts. --- hwilib/devices/digitalbitbox.py | 71 +++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 301770813..dea1a5ca8 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -31,8 +31,15 @@ CTransaction, ExtendedKey, hash256, + is_p2pk, + is_p2pkh, + is_p2sh, + is_p2wpkh, + is_p2wsh, + is_witness, ser_sig_der, ser_sig_compact, + ser_string, ser_compact_size, ) from ..base58 import ( @@ -365,18 +372,39 @@ def sign_tx(self, tx): sighash_tuples = [] for txin, psbt_in, i_num in zip(blank_tx.vin, tx.inputs, range(len(blank_tx.vin))): sighash = b"" + utxo = None + if psbt_in.witness_utxo: + utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: + if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: + raise BadArgumentError('Input {} has a non_witness_utxo with the wrong hash'.format(i_num)) utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] + if utxo is None: + continue + scriptcode = utxo.scriptPubKey + + # Check if P2SH + p2sh = False + if is_p2sh(scriptcode): + # Look up redeemscript + if len(psbt_in.redeem_script) == 0: + continue + scriptcode = psbt_in.redeem_script + p2sh = True + + is_wit, _, _ = is_witness(scriptcode) + + # Check if P2WSH + if is_p2wsh(scriptcode): + # Look up witnessscript + if len(psbt_in.witness_script) == 0: + continue + scriptcode = psbt_in.witness_script - # Check if P2SH - if utxo.is_p2sh(): - # Look up redeemscript - redeemscript = psbt_in.redeem_script + if not is_wit: + if p2sh or is_p2pkh(scriptcode) or is_p2pk(scriptcode): # Add to blank_tx - txin.scriptSig = redeemscript - # Check if P2PKH - elif utxo.is_p2pkh() or utxo.is_p2pk(): - txin.scriptSig = psbt_in.non_witness_utxo.vout[txin.prevout.n].scriptPubKey + txin.scriptSig = scriptcode # We don't know what this is, skip it else: continue @@ -388,7 +416,7 @@ def sign_tx(self, tx): # Hash it sighash += hash256(ser_tx) txin.scriptSig = b"" - elif psbt_in.witness_utxo: + else: # Calculate hashPrevouts and hashSequence prevouts_preimage = b"" sequence_preimage = b"" @@ -404,25 +432,10 @@ def sign_tx(self, tx): outputs_preimage += output.serialize() hashOutputs = hash256(outputs_preimage) - # Get the scriptCode - scriptCode = b"" - witness_program = b"" - if psbt_in.witness_utxo.is_p2sh(): - # Look up redeemscript - redeemscript = psbt_in.redeem_script - witness_program = redeemscript - else: - witness_program = psbt_in.witness_utxo.scriptPubKey - - # Check if witness_program is script hash - if len(witness_program) == 34 and witness_program[0] == 0x00 and witness_program[1] == 0x20: - # look up witnessscript and set as scriptCode - witnessscript = psbt_in.witness_script - scriptCode += ser_compact_size(len(witnessscript)) + witnessscript - else: - scriptCode += b"\x19\x76\xa9\x14" - scriptCode += witness_program[2:] - scriptCode += b"\x88\xac" + # Check if scriptcode is p2wpkh + if is_p2wpkh(scriptcode): + _, _, wit_prog = is_witness(scriptcode) + scriptcode = b"\x76\xa9\x14" + wit_prog + b"\x88\xac" # Make sighash preimage preimage = b"" @@ -430,7 +443,7 @@ def sign_tx(self, tx): preimage += hashPrevouts preimage += hashSequence preimage += txin.prevout.serialize() - preimage += scriptCode + preimage += ser_string(scriptcode) preimage += struct.pack(" Date: Fri, 5 Jun 2020 00:37:16 -0400 Subject: [PATCH 120/634] trezor: Determine segwit based on scripts instead of provided info --- hwilib/devices/trezor.py | 57 +++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 979ea9570..6032f9920 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -45,6 +45,10 @@ from ..serializations import ( CTxOut, ExtendedKey, + is_p2pkh, + is_p2sh, + is_p2wsh, + is_witness, ser_uint256, ) from .. import bech32 @@ -200,26 +204,46 @@ def sign_tx(self, tx): # Detrermine spend type scriptcode = b'' + utxo = None if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo - # Check if the output is p2sh - if psbt_in.witness_utxo.is_p2sh(): + if psbt_in.non_witness_utxo: + if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: + raise BadArgumentError('Input {} has a non_witness_utxo with the wrong hash'.format(input_num)) + utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] + if utxo is None: + continue + scriptcode = utxo.scriptPubKey + + # Check if P2SH + p2sh = False + if is_p2sh(scriptcode): + # Look up redeemscript + if len(psbt_in.redeem_script) == 0: + continue + scriptcode = psbt_in.redeem_script + p2sh = True + + # Check segwit + is_wit, _, _ = is_witness(scriptcode) + + if is_wit: + if p2sh: txinputtype.script_type = proto.InputScriptType.SPENDP2SHWITNESS else: txinputtype.script_type = proto.InputScriptType.SPENDWITNESS - scriptcode = psbt_in.witness_utxo.scriptPubKey - txinputtype.amount = psbt_in.witness_utxo.nValue - elif psbt_in.non_witness_utxo: - utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] + else: txinputtype.script_type = proto.InputScriptType.SPENDADDRESS - scriptcode = utxo.scriptPubKey - txinputtype.amount = psbt_in.non_witness_utxo.vout[txin.prevout.n].nValue - - # Set the script - if psbt_in.witness_script: + txinputtype.amount = utxo.nValue + + # Check if P2WSH + p2wsh = False + if is_p2wsh(scriptcode): + # Look up witnessscript + if len(psbt_in.witness_script) == 0: + continue scriptcode = psbt_in.witness_script - elif psbt_in.redeem_script: - scriptcode = psbt_in.redeem_script + p2wsh = True def ignore_input(): txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0)] @@ -240,11 +264,11 @@ def ignore_input(): # Cannot sign bare multisig, ignore it ignore_input() continue - elif not is_ms and psbt_in.non_witness_utxo and not utxo.is_p2pkh: + elif not is_ms and not is_wit and not is_p2pkh(scriptcode): # Cannot sign unknown spk, ignore it ignore_input() continue - elif not is_ms and psbt_in.witness_utxo and psbt_in.witness_script: + elif not is_ms and is_wit and p2wsh: # Cannot sign unknown witness script, ignore it ignore_input() continue @@ -273,7 +297,8 @@ def ignore_input(): ignore_input() continue elif not found and found_in_sigs: # All of our keys are in partial_sigs, ignore whatever signature is produced for this input - to_ignore.append(input_num) + ignore_input() + continue # append to inputs inputs.append(txinputtype) From c08507737ec6b634ddcc4cb77a896b765ef81bee Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Jun 2020 14:12:22 -0400 Subject: [PATCH 121/634] tests: increase signtx keypool --- test/test_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index ca283a155..fa2dce9ab 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -335,10 +335,10 @@ def _generate_and_finalize(self, unknown_inputs, psbt): def _test_signtx(self, input_type, multisig, external): # Import some keys to the watch only wallet and send coins to them - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '40']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '50']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '30', '40']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '30', '50']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') From 9800899e6cae52b86505a9963b88fc635fabae57 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Jun 2020 21:58:20 -0400 Subject: [PATCH 122/634] ledger: Determine segwit based on scripts and allow both UTXOs Determine whether the input is segwit based on the script. Also handle when both non_witness_utxo and witness_utxo are provided. --- hwilib/devices/ledger.py | 78 +++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index ced716dbf..a51ed19cf 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -26,6 +26,10 @@ ExtendedKey, hash256, hash160, + is_p2sh, + is_p2wpkh, + is_p2wsh, + is_witness, CTransaction, ) import logging @@ -207,59 +211,54 @@ def sign_tx(self, tx): seq.reverse() seq_hex = ''.join('{:02x}'.format(x) for x in seq) + scriptcode = b"" + utxo = None + if psbt_in.witness_utxo: + utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: - segwit_inputs.append({"value": txin.prevout.serialize() + struct.pack(" 0: - # p2wpkh - scriptCode += b"\x76\xa9\x14" - scriptCode += witness_program[2:] - scriptCode += b"\x88\xac" - elif len(witness_program) == 0: - if len(redeemscript) > 0: - scriptCode = redeemscript - else: - scriptCode = psbt_in.non_witness_utxo.vout[txin.prevout.n].scriptPubKey - # Save scriptcode for later signing - script_codes[i_num] = scriptCode + script_codes[i_num] = scriptcode # Find which pubkeys could sign this input (should be all?) for pubkey in psbt_in.hd_keypaths.keys(): - if hash160(pubkey) in scriptCode or pubkey in scriptCode: + if hash160(pubkey) in scriptcode or pubkey in scriptcode: pubkeys.append(pubkey) # Figure out which keys in inputs are from our wallet @@ -287,9 +286,6 @@ def sign_tx(self, tx): # For each input we control do segwit signature for i in range(len(segwit_inputs)): - # Don't try to sign legacy inputs - if tx.inputs[i].non_witness_utxo is not None: - continue for signature_attempt in all_signature_attempts[i]: self.app.startUntrustedTransaction(False, 0, [segwit_inputs[i]], script_codes[i], c_tx.nVersion) tx.inputs[i].partial_sigs[signature_attempt[1]] = self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01) From ce5e09574805f3a40b1492977c860b11b0df912d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Jun 2020 15:15:31 -0400 Subject: [PATCH 123/634] btchip: prefer trustedInput over witness Check if an input is a trustedInput before checking whether it is a witness when determining the flag --- hwilib/devices/btchip/btchip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index 3627f665e..cf643abbd 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -203,10 +203,10 @@ def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, rede apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] params = [] script = bytearray(redeemScript) - if ('witness' in passedOutput) and passedOutput['witness']: - params.append(0x02) - elif ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: params.append(0x01) + elif ('witness' in passedOutput) and passedOutput['witness']: + params.append(0x02) else: params.append(0x00) if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: From fe0f82a143ebc7d934f025b48225a32a8b987be1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Jun 2020 15:16:05 -0400 Subject: [PATCH 124/634] btchip: Give the major, minor, and patch versions separately --- hwilib/devices/btchip/btchip.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index cf643abbd..d7c1af03a 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -397,5 +397,8 @@ def getFirmwareVersion(self): raise result['compressedKeys'] = (response[0] == 0x01) result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) + result['major_version'] = response[2] + result['minor_version'] = response[3] + result['patch_version'] = response[4] result['specialVersion'] = response[1] return result From 45dbd083ad039f3ca9dbe5a1347d2299301cc0e3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Jun 2020 16:28:24 -0400 Subject: [PATCH 125/634] ledger: For app v1.4.0+, do getTrustedInput on segwit inputs --- hwilib/devices/ledger.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index a51ed19cf..22b987870 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -176,6 +176,10 @@ def sign_tx(self, tx): # An entry per input, each with 0 to many keys to sign with all_signature_attempts = [[]] * len(c_tx.vin) + # Get the app version to determine whether to use Trusted Input for segwit + version = self.app.getFirmwareVersion() + use_trusted_segwit = (version['major_version'] == 1 and version['minor_version'] >= 4) or version['major_version'] > 1 + # NOTE: We only support signing Segwit inputs, where we can skip over non-segwit # inputs, or non-segwit inputs, where *all* inputs are non-segwit. This is due # to Ledger's mutually exclusive signing steps for each type. @@ -250,6 +254,10 @@ def sign_tx(self, tx): legacy_inputs[-1]["sequence"] = seq_hex has_legacy = True + if psbt_in.non_witness_utxo and use_trusted_segwit: + ledger_prevtx = bitcoinTransaction(psbt_in.non_witness_utxo.serialize()) + segwit_inputs[-1].update(self.app.getTrustedInput(ledger_prevtx, txin.prevout.n)) + pubkeys = [] signature_attempts = [] @@ -279,7 +287,7 @@ def sign_tx(self, tx): # Process them up front with all scriptcodes blank blank_script_code = bytearray() for i in range(len(segwit_inputs)): - self.app.startUntrustedTransaction(i == 0, i, segwit_inputs, blank_script_code, c_tx.nVersion) + self.app.startUntrustedTransaction(i == 0, i, segwit_inputs, script_codes[i] if use_trusted_segwit else blank_script_code, c_tx.nVersion) # Number of unused fields for Nano S, only changepath and transaction in bytes req self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes) From 427e5d446cddc81da227d754ec4defb7636f6c69 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Jun 2020 23:27:53 -0400 Subject: [PATCH 126/634] travis: Remove macOS binary upload --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9dbbfc5e7..225045f45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -118,9 +118,6 @@ jobs: stage: test os: osx language: generic - addons: - artifacts: - working_dir: dist before_install: install: - brew update && brew upgrade pyenv From 1613a33d65d3fff42c49265c1688423708650da7 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Jun 2020 14:24:57 -0400 Subject: [PATCH 127/634] trezorlib: Raise a more specific error when a prevtx is not provided --- hwilib/devices/trezorlib/btc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index 3104693f7..c0059e829 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -14,6 +14,8 @@ # You should have received a copy of the License along with this library. # If not, see . +import binascii + from . import messages from .tools import CallException, expect, normalize_nfc, session @@ -119,6 +121,8 @@ def copy_tx_meta(tx): # Device asked for one more information, let's process it. if res.details.tx_hash is not None: + if res.details.tx_hash not in prev_txes: + raise ValueError('Previous transaction {} not available'.format(binascii.hexlify(res.details.tx_hash))) current_tx = prev_txes[res.details.tx_hash] else: current_tx = this_tx From 5862228b18daae7082a552d9504fd835a301d256 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 22 Jun 2020 19:41:42 -0400 Subject: [PATCH 128/634] Update Coldcard simulator patches These got out of date and seem to be causing travis failures. --- test/data/coldcard-linux-sock.patch | 12 ++++++------ test/data/coldcard-multisig-setup.patch | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/data/coldcard-linux-sock.patch b/test/data/coldcard-linux-sock.patch index b87c0324c..585fbd797 100644 --- a/test/data/coldcard-linux-sock.patch +++ b/test/data/coldcard-linux-sock.patch @@ -1,20 +1,20 @@ -From d1a3a1cef890ebe4ff72a8f89cd6c56dca89747e Mon Sep 17 00:00:00 2001 +From b0513638e494afa6414a73e2b56f282aba8325cb 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 +Subject: [PATCH 1/2] 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 +index 0fd96de..3ac83f6 100644 --- a/unix/frozen-modules/pyb.py +++ b/unix/frozen-modules/pyb.py -@@ -23,10 +23,10 @@ class USB_HID: - import usocket as socket +@@ -25,10 +25,10 @@ class USB_HID: fn = b'/tmp/ckcc-simulator.sock' self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + # If on linux, try commenting the following line - 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 @@ -26,5 +26,5 @@ index 39778a2..0108516 100644 try: self.pipe.bind(addr) -- -2.11.0 +2.27.0 diff --git a/test/data/coldcard-multisig-setup.patch b/test/data/coldcard-multisig-setup.patch index 9ada99967..151353b59 100644 --- a/test/data/coldcard-multisig-setup.patch +++ b/test/data/coldcard-multisig-setup.patch @@ -1,17 +1,17 @@ -From 86e11ded21ffe839cb907e2096fd7bc832c79ce3 Mon Sep 17 00:00:00 2001 +From cadbd3d25306b43060fd06eed589947d537a5ced Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 17:56:05 -0500 -Subject: [PATCH] Change default simulator multisig +Subject: [PATCH 2/2] Change default simulator multisig --- unix/frozen-modules/sim_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py -index db78972..9565078 100644 +index 0313c3e..e2c3d71 100644 --- a/unix/frozen-modules/sim_settings.py +++ b/unix/frozen-modules/sim_settings.py -@@ -62,7 +62,11 @@ if '-m' in sys.argv: +@@ -68,7 +68,11 @@ if '--ms' in sys.argv: sim_defaults['multisig'] = [["CC-2-of-4", [2, 4], [[1130956047, "tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP"], [3503269483, "tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax"], [2389277556, "tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM"], [3190206587, "tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt"]], {"pp": "48'/1'/0'/1'", "ch": "XTN", "ft": 26}]] else: # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator @@ -25,5 +25,5 @@ index db78972..9565078 100644 if '--xfp' in sys.argv: -- -2.24.1 +2.27.0 From da9e2e0435fce2ca10c9da0eee8b5088157563e3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 22 Jun 2020 20:46:35 -0400 Subject: [PATCH 129/634] tests: update bitcoind segwit fixes patch --- test/data/psbt_non_witness_utxo_segwit.patch | 155 ++++++------------- 1 file changed, 47 insertions(+), 108 deletions(-) diff --git a/test/data/psbt_non_witness_utxo_segwit.patch b/test/data/psbt_non_witness_utxo_segwit.patch index 49b1f7967..46719759d 100644 --- a/test/data/psbt_non_witness_utxo_segwit.patch +++ b/test/data/psbt_non_witness_utxo_segwit.patch @@ -1,7 +1,7 @@ -From 30477b10795dc3e819cc852823f2e00e4e23f770 Mon Sep 17 00:00:00 2001 +From 2789417a7ef31d4b58b2f80783e81a3518dd49af Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:25 -0400 -Subject: [PATCH 1/3] rpc: show both UTXOs in decodepsbt +Subject: [PATCH 1/4] rpc: show both UTXOs in decodepsbt --- src/rpc/rawtransaction.cpp | 9 +++++++-- @@ -45,10 +45,10 @@ index e14217c307..45cf6be3a0 100644 2.27.0 -From fcf1ef6529a010d05a79f9ba6dbfbc480de99af5 Mon Sep 17 00:00:00 2001 +From 8b7d74fd0e6d3180be0dfb315b4e190f0ad0057d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:39 -0400 -Subject: [PATCH 2/3] psbt: Allow both non_witness_utxo and witness_utxo +Subject: [PATCH 2/4] psbt: Allow both non_witness_utxo and witness_utxo --- src/psbt.cpp | 29 ----------------------------- @@ -226,20 +226,20 @@ index 89737ca7b5..054b312cd7 100644 2.27.0 -From 946b5c010fa23103c1ee5f584bd5be240e42c4c9 Mon Sep 17 00:00:00 2001 +From a656eb32d35d2fdc1df5b8d55eba6a0c3efd1c2f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:43 -0400 -Subject: [PATCH 3/3] psbt: always put a non_witness_utxo and don't remove it +Subject: [PATCH 3/4] psbt: always put a non_witness_utxo and don't remove it Offline signers will always need a non_witness_utxo so make sure it is there. --- - src/psbt.cpp | 7 +- - src/psbt.h | 4 +- - src/wallet/test/psbt_wallet_tests.cpp | 2 +- - src/wallet/wallet.cpp | 2 +- - test/functional/rpc_psbt.py | 94 ++++++++++++++------------- - 5 files changed, 56 insertions(+), 53 deletions(-) + src/psbt.cpp | 7 ++++--- + src/psbt.h | 4 ++-- + src/wallet/test/psbt_wallet_tests.cpp | 2 +- + src/wallet/wallet.cpp | 2 +- + test/functional/rpc_psbt.py | 4 +++- + 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/psbt.cpp b/src/psbt.cpp index 4c8b40ca0b..51f829d533 100644 @@ -315,108 +315,18 @@ index 054b312cd7..7816a39ec1 100644 const auto it = mapWallet.find(txhash); if (it != mapWallet.end()) { diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py -index 51d136d26a..735293bbc7 100755 +index 9b07c39606..2fe11ef116 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py -@@ -37,51 +37,52 @@ class PSBTTest(BitcoinTestFramework): +@@ -37,6 +37,7 @@ class PSBTTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() -- def test_utxo_conversion(self): -- mining_node = self.nodes[2] -- offline_node = self.nodes[0] -- online_node = self.nodes[1] -- -- # Disconnect offline node from others -- disconnect_nodes(offline_node, 1) -- disconnect_nodes(online_node, 0) -- disconnect_nodes(offline_node, 2) -- disconnect_nodes(mining_node, 0) -- -- # Create watchonly on online_node -- online_node.createwallet(wallet_name='wonline', disable_private_keys=True) -- wonline = online_node.get_wallet_rpc('wonline') -- w2 = online_node.get_wallet_rpc('') -- -- # Mine a transaction that credits the offline address -- offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit") -- online_addr = w2.getnewaddress(address_type="p2sh-segwit") -- wonline.importaddress(offline_addr, "", False) -- mining_node.sendtoaddress(address=offline_addr, amount=1.0) -- mining_node.generate(nblocks=1) -- self.sync_blocks([mining_node, online_node]) -- -- # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) -- utxos = wonline.listunspent(addresses=[offline_addr]) -- raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}]) -- psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"] -- assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] -- -- # Have the offline node sign the PSBT (which will update the UTXO to segwit) -- signed_psbt = offline_node.walletprocesspsbt(psbt)["psbt"] -- assert "witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] -- -- # Make sure we can mine the resulting transaction -- txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) -- mining_node.generate(1) -- self.sync_blocks([mining_node, online_node]) -- assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) -- -- wonline.unloadwallet() -- -- # Reconnect -- connect_nodes(self.nodes[0], 1) -- connect_nodes(self.nodes[0], 2) + # TODO: Re-enable this test with segwit v1 -+ # def test_utxo_conversion(self): -+ # mining_node = self.nodes[2] -+ # offline_node = self.nodes[0] -+ # online_node = self.nodes[1] -+ # -+ # # Disconnect offline node from others -+ # disconnect_nodes(offline_node, 1) -+ # disconnect_nodes(online_node, 0) -+ # disconnect_nodes(offline_node, 2) -+ # disconnect_nodes(mining_node, 0) -+ # -+ # # Create watchonly on online_node -+ # online_node.createwallet(wallet_name='wonline', disable_private_keys=True) -+ # wonline = online_node.get_wallet_rpc('wonline') -+ # w2 = online_node.get_wallet_rpc('') -+ # -+ # # Mine a transaction that credits the offline address -+ # offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit") -+ # online_addr = w2.getnewaddress(address_type="p2sh-segwit") -+ # wonline.importaddress(offline_addr, "", False) -+ # mining_node.sendtoaddress(address=offline_addr, amount=1.0) -+ # mining_node.generate(nblocks=1) -+ # self.sync_blocks([mining_node, online_node]) -+ # -+ # # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) -+ # utxos = wonline.listunspent(addresses=[offline_addr]) -+ # raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}]) -+ # psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"] -+ # assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] -+ # -+ # # Have the offline node sign the PSBT (which will update the UTXO to segwit) -+ # signed_psbt = offline_node.walletprocesspsbt(psbt)["psbt"] -+ # assert "witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] -+ # -+ # # Make sure we can mine the resulting transaction -+ # txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) -+ # mining_node.generate(1) -+ # self.sync_blocks([mining_node, online_node]) -+ # assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) -+ # -+ # wonline.unloadwallet() -+ # -+ # # Reconnect -+ # connect_nodes(self.nodes[0], 1) -+ # connect_nodes(self.nodes[0], 2) - - def run_test(self): - # Create and fund a raw tx for sending 10 BTC -@@ -346,7 +347,8 @@ class PSBTTest(BitcoinTestFramework): + def test_utxo_conversion(self): + mining_node = self.nodes[2] + offline_node = self.nodes[0] +@@ -344,7 +345,8 @@ class PSBTTest(BitcoinTestFramework): for i, signer in enumerate(signers): self.nodes[2].unloadwallet("wallet{}".format(i)) @@ -429,3 +339,32 @@ index 51d136d26a..735293bbc7 100755 -- 2.27.0 + +From 836d6fc375ae8709c6175d36f46df365776a497c Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Mon, 8 Jun 2020 19:27:16 -0400 +Subject: [PATCH 4/4] tests: Check that segwit inputs in psbt have both UTXO + types + +--- + test/functional/rpc_psbt.py | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py +index 2fe11ef116..df7d501e5b 100755 +--- a/test/functional/rpc_psbt.py ++++ b/test/functional/rpc_psbt.py +@@ -149,6 +149,10 @@ class PSBTTest(BitcoinTestFramework): + # spend single key from node 1 + rawtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99})['psbt'] + walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(rawtx) ++ # Make sure it has both types of UTXOs ++ decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) ++ assert 'non_witness_utxo' in decoded['inputs'][0] ++ assert 'witness_utxo' in decoded['inputs'][0] + assert_equal(walletprocesspsbt_out['complete'], True) + self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) + +-- +2.27.0 + From 5083bf4e6bbddc1525a482a824849fcf6297c566 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jun 2020 22:03:59 +0300 Subject: [PATCH 130/634] Support multisig `displayaddress` (Trezor) --- hwilib/cli.py | 3 +- hwilib/commands.py | 4 +-- hwilib/devices/digitalbitbox.py | 4 +-- hwilib/devices/ledger.py | 5 ++- hwilib/devices/trezor.py | 57 ++++++++++++++++++++++++++------- hwilib/hwwclient.py | 5 +++ 6 files changed, 60 insertions(+), 18 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 6d83d10ce..fd25d2a31 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -40,7 +40,7 @@ def backup_device_handler(args, client): return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) def displayaddress_handler(args, client): - return displayaddress(client, desc=args.desc, path=args.path, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh) + return displayaddress(client, desc=args.desc, path=args.path, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh, redeem_script=args.redeem_script) def enumerate_handler(args): return enumerate(password=args.password) @@ -177,6 +177,7 @@ def process_commands(cli_args): group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. m/84h/0h/0h/1/*') displayaddr_parser.add_argument('--sh_wpkh', action='store_true', help='Display the p2sh-nested segwit address associated with this key path') displayaddr_parser.add_argument('--wpkh', action='store_true', help='Display the bech32 version of the address associated with this key path') + displayaddr_parser.add_argument('--redeem_script', help='P2SH redeem script') displayaddr_parser.set_defaults(func=displayaddress_handler) setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p. Requires interactive mode') diff --git a/hwilib/commands.py b/hwilib/commands.py index df59df1c2..d0d7e064c 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -220,11 +220,11 @@ def getdescriptors(client, account=0): return result -def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False): +def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, redeem_script=None): if path is not None: if sh_wpkh and wpkh: return {'error': 'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.', 'code': BAD_ARGUMENT} - return client.display_address(path, sh_wpkh, wpkh) + return client.display_address(path, sh_wpkh, wpkh, redeem_script=redeem_script) elif desc is not None: if sh_wpkh or wpkh: return {'error': ' `--wpkh` and `--sh_wpkh` can not be combined with --desc', 'code': BAD_ARGUMENT} diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index dea1a5ca8..5ff6d7fce 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -548,8 +548,8 @@ def sign_message(self, message, keypath): return {"signature": base64.b64encode(compact_sig).decode('utf-8')} - # Display address of specified type on the device. Only supports single-key based addresses. - def display_address(self, keypath, p2sh_p2wpkh, bech32): + # Display address of specified type on the device. + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') # Setup a new device diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 22b987870..0295de09f 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -338,10 +338,13 @@ def sign_message(self, message, keypath): return {"signature": base64.b64encode(sig).decode('utf-8')} + # Display address of specified type on the device. Only supports single-key based addresses. @ledger_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") + if redeem_script is not None: + raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") output = self.app.getWalletPublicKey(keypath[2:], True, (p2sh_p2wpkh or bech32), bech32) return {'address': output['address'][12:-2]} # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index f27022967..450fcfb90 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -408,20 +408,53 @@ def sign_message(self, message, keypath): result = btc.sign_message(self.client, self.coin_name, path, message) return {'signature': base64.b64encode(result.signature).decode('utf-8')} - # Display address of specified type on the device. Only supports single-key based addresses. + # Display address of specified type on the device. @trezor_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): self._check_unlocked() - expanded_path = tools.parse_path(keypath) - script_type = proto.InputScriptType.SPENDWITNESS if bech32 else (proto.InputScriptType.SPENDP2SHWITNESS if p2sh_p2wpkh else proto.InputScriptType.SPENDADDRESS) - address = btc.get_address( - self.client, - self.coin_name, - expanded_path, - show_display=True, - script_type=script_type - ) - return {'address': address} + + # redeem_script means p2sh/multisig + if redeem_script: + # Get multisig object required by Trezor's get_address + multisig = parse_multisig(bytes.fromhex(redeem_script)) + if not multisig[0]: + raise BadArgumentError("The redeem script provided is not a multisig. Only multisig scripts can be displayed.") + multisig = multisig[1] + else: + multisig = None + + # Script type + if p2sh_p2wpkh: + script_type = proto.InputScriptType.SPENDP2SHWITNESS + elif bech32: + script_type = proto.InputScriptType.SPENDWITNESS + elif redeem_script: + script_type = proto.InputScriptType.SPENDMULTISIG + else: + script_type = proto.InputScriptType.SPENDADDRESS + + # convert device fingerprint to 'm' if exists in path + keypath = keypath.replace(self.get_master_fingerprint_hex(), 'm') + + for path in keypath.split(','): + if len(path.split('/')[0]) == 8: + path = path.split('/', 1)[1] + expanded_path = tools.parse_path(path) + + try: + address = btc.get_address( + self.client, + self.coin_name, + expanded_path, + show_display=True, + script_type=script_type, + multisig=multisig, + ) + return {'address': address} + except: + pass + + raise BadArgumentError("No path supplied matched device keys") # Setup a new device @trezor_exception diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 1c7a12359..a7c76b67c 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -41,6 +41,11 @@ def sign_message(self, message, keypath): raise NotImplementedError('The HardwareWalletClient base class does not ' 'implement this method') + # Display address of specified type on the device. + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): + raise NotImplementedError('The HardwareWalletClient base class does not ' + 'implement this method') + # Setup a new device def setup_device(self, label='', passphrase=''): raise NotImplementedError('The HardwareWalletClient base class does not ' From 59f8c1d56aca2210b448b62b20d5cdc4d174b995 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jun 2020 22:04:22 +0300 Subject: [PATCH 131/634] Support multisig `displayaddress` (ColdCard) --- hwilib/devices/ckcc/protocol.py | 22 +++++++++++++ hwilib/devices/ckcc/utils.py | 20 ++++++++++++ hwilib/devices/coldcard.py | 55 ++++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/hwilib/devices/ckcc/protocol.py b/hwilib/devices/ckcc/protocol.py index 8467cba69..e52e58801 100644 --- a/hwilib/devices/ckcc/protocol.py +++ b/hwilib/devices/ckcc/protocol.py @@ -109,6 +109,28 @@ def show_address(subpath, addr_fmt=AF_CLASSIC): # shows on screen, no feedback from user expected return pack('<4sI', b'show', addr_fmt) + subpath.encode('ascii') + @staticmethod + def show_p2sh_address(M, xfp_paths, witdeem_script, addr_fmt=AF_P2SH): + # For multisig (aka) P2SH cases, you will need all the info required to build + # the redeem script, and the Coldcard must already have been enrolled + # into the wallet. + # - redeem script must be provided + # - full subkey paths for each involved key is required in a list of lists of ints, where + # is a XFP and derivation path, like in BIP174 + # - the order of xfp_paths must match the order of pubkeys in + # redeem script (after BIP67 sort). This allows for dup xfp values. + assert addr_fmt & AFC_SCRIPT + assert 30 <= len(witdeem_script) <= 520 + + rv = pack('<4sIBBH', b'p2sh', addr_fmt, M, len(xfp_paths), len(witdeem_script)) + rv += witdeem_script + + for xfp_path in xfp_paths: + ln = len(xfp_path) + rv += pack('= 2, i + here = int(i[:-1]) | 0x80000000 + else: + here = int(i) + assert 0 <= here < 0x80000000, here + + rv.append(here) + + return rv + # EOF diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 2c21c3bad..7848d1bab 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -1,6 +1,5 @@ # Coldcard interaction script -from binascii import b2a_hex from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -27,6 +26,12 @@ AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, + AF_P2WSH, + AF_P2SH, + AF_P2WSH_P2SH, +) +from .ckcc.utils import ( + str_to_int_path, ) from ..base58 import ( get_xpub_fingerprint, @@ -44,7 +49,7 @@ import sys import time import struct -from binascii import hexlify +from binascii import hexlify, a2b_hex, b2a_hex CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock' # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md @@ -198,20 +203,54 @@ def sign_message(self, message, keypath): sig = str(base64.b64encode(raw), 'ascii').replace('\n', '') return {"signature": sig} - # Display address of specified type on the device. Only supports single-key based addresses. + # Display address of specified type on the device. @coldcard_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') if p2sh_p2wpkh: - format = AF_P2WPKH_P2SH + addr_fmt = AF_P2WSH_P2SH if redeem_script else AF_P2WPKH_P2SH elif bech32: - format = AF_P2WPKH + addr_fmt = AF_P2WSH if redeem_script else AF_P2WPKH else: - format = AF_CLASSIC - address = self.device.send_recv(CCProtocolPacker.show_address(keypath, format), timeout=None) + addr_fmt = AF_P2SH if redeem_script else AF_CLASSIC + + if redeem_script: + keypaths = keypath.split(',') + script = a2b_hex(redeem_script) + + N = len(keypaths) + + if not 1 <= N <= 15: + raise BadArgumentError("Must provide 1 to 15 keypaths to display a multisig address") + + min_signers = script[0] - 80 + if not 1 <= min_signers <= N: + raise BadArgumentError("Either the redeem script provided is invalid or the keypaths provided are insufficient") + + if not script[-1] == 0xAE: + raise BadArgumentError("The redeem script provided is not a multisig. Only multisig scripts can be displayed.") + + if not script[-2] == 80 + N: + raise BadArgumentError("Invalid redeem script, second last byte should encode N") + + xfp_paths = [] + for xfp in keypaths: + if '/' not in xfp: + raise BadArgumentError('Invalid keypath. Needs a XFP/path: ' + xfp) + xfp, p = xfp.split('/', 1) + + xfp_paths.append(str_to_int_path(xfp, p)) + + payload = CCProtocolPacker.show_p2sh_address(min_signers, xfp_paths, script, addr_fmt=addr_fmt) + # single-sig + else: + payload = CCProtocolPacker.show_address(keypath, addr_fmt=addr_fmt) + + address = self.device.send_recv(payload, timeout=None) + if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) return {'address': address} From a25baf858d4b4ecff0d0baa3fd59e0951d9690ef Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jun 2020 22:04:31 +0300 Subject: [PATCH 132/634] Support multisig descriptors in `displayaddress` --- hwilib/commands.py | 14 ++++++ hwilib/descriptor.py | 116 +++++++++++++++++++++++++++++++++---------- 2 files changed, 105 insertions(+), 25 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index d0d7e064c..28cb69f21 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -228,9 +228,23 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede elif desc is not None: if sh_wpkh or wpkh: return {'error': ' `--wpkh` and `--sh_wpkh` can not be combined with --desc', 'code': BAD_ARGUMENT} + if redeem_script: + return {'error': ' `--redeem_script` can not be combined with --desc', 'code': BAD_ARGUMENT} descriptor = Descriptor.parse(desc, client.is_testnet) if descriptor is None: return {'error': 'Unable to parse descriptor: ' + desc, 'code': BAD_ARGUMENT} + if descriptor.sh or descriptor.sh_wsh or descriptor.wsh: + path = '' + redeem_script = format(80 + int(descriptor.multisig_M), 'x') + for i in range(0, descriptor.multisig_N): + path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] + ',' + if not descriptor.path_suffix[i]: + redeem_script += '21' + descriptor.base_key[i] + else: + return {'error': 'Multisig descriptor must include all pubkeys', 'code': BAD_ARGUMENT} + path = path[0:-1] + redeem_script += format(80 + descriptor.multisig_N, 'x') + 'ae' + return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script) if descriptor.m_path is None: return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} if descriptor.origin_fingerprint != client.get_master_fingerprint_hex(): diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 5c6eb5bd9..85f4fddd2 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -50,7 +50,21 @@ def AddChecksum(desc): return desc + "#" + DescriptorChecksum(desc) class Descriptor: - def __init__(self, origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh): + def __init__( + self, + origin_fingerprint, + origin_path, + base_key, + path_suffix, + testnet, + sh_wpkh=None, + wpkh=None, + sh=None, + sh_wsh=None, + wsh=None, + multisig_M=None, + multisig_N=None + ): self.origin_fingerprint = origin_fingerprint self.origin_path = origin_path self.path_suffix = path_suffix @@ -58,9 +72,14 @@ def __init__(self, origin_fingerprint, origin_path, base_key, path_suffix, testn self.testnet = testnet self.sh_wpkh = sh_wpkh self.wpkh = wpkh + self.sh = sh + self.sh_wsh = sh_wsh + self.wsh = wsh + self.multisig_M = multisig_M + self.multisig_N = multisig_N self.m_path = None - if origin_path: + if origin_path and not isinstance(origin_path, list): self.m_path_base = "m" + origin_path self.m_path = "m" + origin_path + (path_suffix or "") @@ -68,11 +87,16 @@ def __init__(self, origin_fingerprint, origin_path, base_key, path_suffix, testn def parse(cls, desc, testnet=False): sh_wpkh = None wpkh = None + sh = None + sh_wsh = None + wsh = None origin_fingerprint = None origin_path = None base_key_and_path_match = None base_key = None path_suffix = None + multisig_M = None + multisig_N = None # Check the checksum check_split = desc.split('#') @@ -92,31 +116,70 @@ def parse(cls, desc, testnet=False): sh_wpkh = True elif desc.startswith("wpkh("): wpkh = True - - origin_match = re.search(r"\[(.*)\]", desc) - if origin_match: - origin = origin_match.group(1) - match = re.search(r"^([0-9a-fA-F]{8})(\/.*)", origin) - if match: - origin_fingerprint = match.group(1) - origin_path = match.group(2) - # Replace h with ' - origin_path = origin_path.replace('h', '\'') - - base_key_and_path_match = re.search(r"\[.*\](\w+)([\/\)][\d'\/\*]*)", desc) + elif desc.startswith("sh(wsh("): + sh_wsh = True + elif desc.startswith("wsh("): + wsh = True + elif desc.startswith("sh("): + sh = True + + if sh or sh_wsh or wsh: + if 'multi(' not in desc: + # only multisig scripts are supported + return None + # get the list of keys only + keys = desc.split(',', 1)[1].split(')', 1)[0].split(',') + if 'sortedmulti' in desc: + keys.sort(key=lambda x: x if ']' not in x else x.split(']')[1]) + multisig_M = desc.split(',')[0].split('(')[-1] + multisig_N = len(keys) else: - base_key_and_path_match = re.search(r"\((\w+)([\/\)][\d'\/\*]*)", desc) - - if base_key_and_path_match: - base_key = base_key_and_path_match.group(1) - path_suffix = base_key_and_path_match.group(2) - if path_suffix == ")": - path_suffix = None + keys = [desc.split('(')[-1].split(')', 1)[0]] + + descriptors = [] + for key in keys: + origin_match = re.search(r"\[(.*)\]", key) + if origin_match: + origin = origin_match.group(1) + match = re.search(r"^([0-9a-fA-F]{8})(\/.*)", origin) + if match: + origin_fingerprint = match.group(1) + origin_path = match.group(2) + # Replace h with ' + origin_path = origin_path.replace('h', '\'') + + base_key_and_path_match = re.search(r"\[.*\](\w+)([\d'\/\*]*)", key) + else: + base_key_and_path_match = re.search(r"(\w+)([\d'\/\*]*)", key) + + if base_key_and_path_match: + base_key = base_key_and_path_match.group(1) + path_suffix = base_key_and_path_match.group(2) + if path_suffix == '': + path_suffix = None + else: + if origin_match is None: + return None + + descriptors.append(cls(origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh, sh, sh_wsh, wsh)) + if len(descriptors) == 1: + return descriptors[0] else: - if origin_match is None: - return None - - return cls(origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh) + # for multisig scripts save as lists all keypaths fields + return cls( + [descriptor.origin_fingerprint for descriptor in descriptors], + [descriptor.origin_path for descriptor in descriptors], + [descriptor.base_key for descriptor in descriptors], + [descriptor.path_suffix for descriptor in descriptors], + testnet, + sh_wpkh, + wpkh, + sh, + sh_wsh, + wsh, + multisig_M, + multisig_N + ) def serialize(self): descriptor_open = 'pkh(' @@ -129,6 +192,9 @@ def serialize(self): elif self.sh_wpkh: descriptor_open = 'sh(wpkh(' descriptor_close = '))' + elif self.sh or self.sh_wsh or self.wsh: + # serialize multisig descriptor is not supported yet. + return None if self.origin_fingerprint and self.origin_path: origin = '[' + self.origin_fingerprint + self.origin_path + ']' From 967138c2db19d6f82368661f7d5dbba5d00c0b0f Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jun 2020 22:04:43 +0300 Subject: [PATCH 133/634] Add test for multisig `displayaddress` --- test/test_device.py | 152 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/test/test_device.py b/test/test_device.py index fa2dce9ab..de033fe1e 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -456,6 +456,11 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def setUp(self): + self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) + if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): + self.rpc.createwallet('{}_test'.format(self.full_type), True) + self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) + self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) if '--testnet' not in self.dev_args: self.dev_args.append('--testnet') self.emulator.start() @@ -533,6 +538,153 @@ def test_display_address_descriptor(self): self.assertIn('code', result) self.assertEqual(result['code'], -7) + def test_display_address_multisig_path(self): + supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} + if self.full_type not in supports_multisig: + return + # Import some keys to the watch only wallet and get multisig address + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '40', '50']) + import_result = self.wrpc.importmulti(keypool_desc) + self.assertTrue(import_result[0]['success']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '40', '50']) + import_result = self.wrpc.importmulti(keypool_desc) + self.assertTrue(import_result[0]['success']) + sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') + wpkh_addr = self.wrpc.getnewaddress('', 'bech32') + pkh_addr = self.wrpc.getnewaddress('', 'legacy') + self.wrpc.importaddress(wpkh_addr) + self.wrpc.importaddress(pkh_addr) + + # pubkeys to construct 2-of-3 multisig descriptors for import + sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) + wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) + pkh_info = self.wrpc.getaddressinfo(pkh_addr) + + pubkeys = [sh_wpkh_info['desc'][8:-11], + wpkh_info['desc'][5:-10], + pkh_info['desc'][4:-10]] + + # Get the descriptors with their checksums + sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] + sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] + wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] + + sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display"} + sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display"} + # re-order pubkeys to allow import without "already have private keys" error + wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display"} + multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) + self.assertTrue(multi_result[0]['success']) + self.assertTrue(multi_result[1]['success']) + self.assertTrue(multi_result[2]['success']) + + sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display").popitem()[0] + sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display").popitem()[0] + wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display").popitem()[0] + + sh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] + sh_wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] + wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] + + path = pubkeys[2][1:24] + ',' + pubkeys[1][1:24] + ',' + pubkeys[0][1:24] + # need to replace `'` with `h` for stdin option to work + path = path.replace("'", "h") + + # legacy + result = self.do_command(self.dev_args + ['displayaddress', '--path', path, '--redeem_script', sh_multi_addr_redeem_script]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + self.assertEqual(sh_multi_addr, result['address']) + + # wrapped segwit + result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', path, '--redeem_script', sh_wsh_multi_addr_redeem_script]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + self.assertEqual(sh_wsh_multi_addr, result['address']) + + # native setwit + result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', path, '--redeem_script', wsh_multi_addr_redeem_script]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56]) + + def test_display_address_multisig_descriptor(self): + supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} + if self.full_type not in supports_multisig: + return + # Import some keys to the watch only wallet and get multisig address + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '50', '60']) + import_result = self.wrpc.importmulti(keypool_desc) + self.assertTrue(import_result[0]['success']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '50', '60']) + import_result = self.wrpc.importmulti(keypool_desc) + self.assertTrue(import_result[0]['success']) + sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') + wpkh_addr = self.wrpc.getnewaddress('', 'bech32') + pkh_addr = self.wrpc.getnewaddress('', 'legacy') + self.wrpc.importaddress(wpkh_addr) + self.wrpc.importaddress(pkh_addr) + + # pubkeys to construct 2-of-3 multisig descriptors for import + sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) + wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) + pkh_info = self.wrpc.getaddressinfo(pkh_addr) + + pubkeys = [sh_wpkh_info['desc'][8:-11], + wpkh_info['desc'][5:-10], + pkh_info['desc'][4:-10]] + + # Get the descriptors with their checksums + sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] + sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] + wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] + + sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display-desc"} + sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display-desc"} + # re-order pubkeys to allow import without "already have private keys" error + wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display-desc"} + multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) + self.assertTrue(multi_result[0]['success']) + self.assertTrue(multi_result[1]['success']) + self.assertTrue(multi_result[2]['success']) + + sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display-desc").popitem()[0] + sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display-desc").popitem()[0] + wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display-desc").popitem()[0] + + # need to replace `'` with `h` and to remove checksome for the stdin option to work + sh_multi_desc = sh_multi_desc.replace("'", "h").split('#')[0] + sh_wsh_multi_desc = sh_wsh_multi_desc.replace("'", "h").split('#')[0] + wsh_multi_desc = wsh_multi_desc.replace("'", "h").split('#')[0] + + # legacy + result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_multi_desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + self.assertEqual(sh_multi_addr, result['address']) + + # wrapped segwit + result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_wsh_multi_desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + self.assertEqual(sh_wsh_multi_addr, result['address']) + + # native setwit + result = self.do_command(self.dev_args + ['displayaddress', '--desc', wsh_multi_desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56]) + class TestSignMessage(DeviceTestCase): def test_sign_msg(self): self.do_command(self.dev_args + ['signmessage', '"Message signing test"', 'm/44h/1h/0h/0/0']) From 94745d9deab15f4b75bb10bc545e472b147ca007 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Jun 2020 11:06:46 -0400 Subject: [PATCH 134/634] release: Bump to 1.1.2-rc.1 --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3b1199496..971f5909f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.1" +version = "1.1.2-rc.1" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index ef04ca6ea..f7895ebca 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.1', + 'version': '1.1.2rc1', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', From 5cd4cce9f0143c61511439daa07e90a6f4a99f4f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Jun 2020 11:22:30 -0400 Subject: [PATCH 135/634] release: Bump to 1.1.2-rc.2 --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index b3ddbc41f..ad190f85a 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.1.1' +__version__ = '1.1.2-rc.2' diff --git a/pyproject.toml b/pyproject.toml index 971f5909f..7689aef27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.2-rc.1" +version = "1.1.2-rc.2" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index f7895ebca..dc274277c 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.2rc1', + 'version': '1.1.2rc2', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', From 4c08a59fbdaded356b75e21dddc3bf40acf28cc6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 29 Jun 2020 11:20:14 -0400 Subject: [PATCH 136/634] release: Bump to 1.1.2 --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index ad190f85a..7b344eca4 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.1.2-rc.2' +__version__ = '1.1.2' diff --git a/pyproject.toml b/pyproject.toml index 7689aef27..533a88c8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.2-rc.2" +version = "1.1.2" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index dc274277c..70d8b1fd3 100644 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.2rc2', + 'version': '1.1.2', 'description': 'A library for working with Bitcoin hardware wallets', 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', From 465f4fd8ed1800cb42197cdff1058af00511ad83 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Sat, 27 Jun 2020 15:06:00 +0200 Subject: [PATCH 137/634] annotated code --- hwilib/hwwclient.py | 237 +++++++++++++++++++++++++++++++------------- 1 file changed, 168 insertions(+), 69 deletions(-) diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index a7c76b67c..bafc84c46 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -1,82 +1,181 @@ +from typing import Dict, Optional, Union + from .base58 import get_xpub_fingerprint_hex +from .serializations import PSBT + -# This is an abstract class that defines all of the methods that each Hardware -# wallet subclass must implement. class HardwareWalletClient(object): + """Create a client for a HID device that has already been opened. + + This abstract class defines the methods + that hardware wallet subclasses should implement. + """ - # device is an HID device that has already been opened. - def __init__(self, path, password, expert): + def __init__(self, path: str, password: str, expert: bool) -> None: self.path = path self.password = password self.message_magic = b"\x18Bitcoin Signed Message:\n" self.is_testnet = False - self.fingerprint = None - self.xpub_cache = {} + self.fingerprint: Optional[str] = None + # {bip32_path: } + self.xpub_cache: Dict[str, str] = {} self.expert = expert - # Get the master BIP 44 pubkey - def get_master_xpub(self): - return self.get_pubkey_at_path('m/44\'/0\'/0\'') + def get_master_xpub(self) -> Dict[str, str]: + """Return the master BIP44 public key. + + Retrieve the public key at the "m/44h/0h/0h" derivation path. + + Return {"xpub": }. + """ + # FIXME testnet is not handled yet + return self.get_pubkey_at_path("m/44h/0h/0h") - # Get the master fingerprint - def get_master_fingerprint_hex(self): - master_xpub = self.get_pubkey_at_path('m/0h')['xpub'] + def get_master_fingerprint_hex(self) -> str: + """Return the master public key fingerprint as hex-string. + + Retrieve the master public key at the "m/0h" derivation path. + """ + master_xpub = self.get_pubkey_at_path("m/0h")["xpub"] return get_xpub_fingerprint_hex(master_xpub) - # Must return a dict with the xpub - # Retrieves the public key at the specified BIP 32 derivation path - def get_pubkey_at_path(self, path): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Must return a hex string with the signed transaction - # The tx must be in the combined unsigned transaction format - def sign_tx(self, tx): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Must return a base64 encoded string with the signed message - # The message can be any string. keypath is the bip 32 derivation path for the key to sign with - def sign_message(self, message, keypath): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Display address of specified type on the device. - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Setup a new device - def setup_device(self, label='', passphrase=''): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Wipe this device - def wipe_device(self): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Restore device from mnemonic or xprv - def restore_device(self, label='', word_count=24): - raise NotImplementedError('The HardwareWalletClient base class does not implement this method') - - # Begin backup process - def backup_device(self, label='', passphrase=''): - raise NotImplementedError('The HardwareWalletClient base class does not implement this method') - - # Close the device - def close(self): - raise NotImplementedError('The HardwareWalletClient base class does not ' - 'implement this method') - - # Prompt pin - def prompt_pin(self): - raise NotImplementedError('The HardwareWalletClient base class does not implement this method') - - # Send pin - def send_pin(self): - raise NotImplementedError('The HardwareWalletClient base class does not implement this method') - - # Toggle passphrase - def toggle_passphrase(self): - raise NotImplementedError('The HardwareWalletClient base class does not implement this method') + def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: + """Return the public key at the BIP32 derivation path. + + Return {"xpub": }. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def sign_tx(self, psbt: PSBT) -> Dict[str, str]: + """Sign a partially signed bitcoin transaction (PSBT). + + Return {"psbt": }. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def sign_message(self, message: str, bip32_path: str) -> Dict[str, str]: + """Sign a message (bitcoin message signing). + + Sign the message according to the bitcoin message signing standard. + + Retrieve the signing key at the specified BIP32 derivation path. + + Return {"signature": }. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def display_address( + self, + bip32_path: str, + p2sh_p2wpkh: bool, + bech32: bool, + redeem_script: Optional[str] = None, + ) -> Dict[str, str]: + """Display and return the address of specified type. + + redeem_script is a hex-string. + + Retrieve the public key at the specified BIP32 derivation path. + + Return {"address": }. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def wipe_device(self) -> Dict[str, Union[bool, str, int]]: + """Wipe the HID device. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def setup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + """Setup the HID device. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": str, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def restore_device( + self, label: str = "", word_count: int = 24 + ) -> Dict[str, Union[bool, str, int]]: + """Restore the HID device from mnemonic. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def backup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + """Backup the HID device. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def close(self) -> None: + "Close the HID device." + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: + """Prompt for PIN. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def send_pin(self) -> Dict[str, Union[bool, str, int]]: + """Send PIN. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + + def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: + """Toggle passphrase. + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") From bd60b745be3499026955f46919cf361904f457dc Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jul 2020 12:26:49 -0400 Subject: [PATCH 138/634] Update btchip library --- hwilib/devices/btchip/__init__.py | 2 +- hwilib/devices/btchip/btchip.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/hwilib/devices/btchip/__init__.py b/hwilib/devices/btchip/__init__.py index 598eef9b8..7e1b55eaa 100644 --- a/hwilib/devices/btchip/__init__.py +++ b/hwilib/devices/btchip/__init__.py @@ -16,4 +16,4 @@ * limitations under the License. ******************************************************************************** """ - +__version__ = "0.1.30" diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index d7c1af03a..697fd1469 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -172,7 +172,7 @@ def getTrustedInput(self, transaction, index): result['value'] = response return result - def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False): + def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False, continueSegwit=False): # Start building a fake transaction with the passed inputs segwit = False if newTransaction: @@ -186,7 +186,7 @@ def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, rede else: p2 = 0x00 else: - p2 = 0x80 + p2 = 0x10 if continueSegwit else 0x80 apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] params = bytearray([version, 0x00, 0x00, 0x00]) writeVarint(len(outputList), params) @@ -215,8 +215,6 @@ def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, rede if currentIndex != inputIndex: script = bytearray() writeVarint(len(script), params) - if len(script) == 0: - params.extend(sequence) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) @@ -234,6 +232,10 @@ def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, rede apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset += blockLength + if len(script) == 0: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] + apdu.extend(sequence) + self.dongle.exchange(bytearray(apdu)) currentIndex += 1 def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): From 55f9c7f61d84a5f785470c067823367e1c01ae49 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jul 2020 13:03:10 -0400 Subject: [PATCH 139/634] Update speculos patch --- test/data/speculos-auto-button.patch | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch index cb3c1e5b0..4d21bb31e 100644 --- a/test/data/speculos-auto-button.patch +++ b/test/data/speculos-auto-button.patch @@ -1,4 +1,4 @@ -From 80038049e33f6a708e1d1c2151ef48e1bce3ee70 Mon Sep 17 00:00:00 2001 +From 73d6aaf4262b083bf4b8860914d9e0edcdff3ea6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 12 Mar 2020 17:25:09 -0400 Subject: [PATCH] Do button presses within seproxyhal @@ -8,18 +8,16 @@ Subject: [PATCH] Do button presses within seproxyhal 1 file changed, 35 insertions(+) diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py -index b18f8a2..b0554ea 100644 +index ffaaff5..eaf22c9 100644 --- a/mcu/seproxyhal.py +++ b/mcu/seproxyhal.py -@@ -2,6 +2,7 @@ import binascii +@@ -1,4 +1,5 @@ import logging - import os - import select +import struct import sys import time import threading -@@ -146,6 +147,7 @@ class SeProxyHal: +@@ -141,6 +142,7 @@ class SeProxyHal: daemon=True) self.ticker_thread.start() self.usb = usb.USB(self.packet_thread.queue_packet) @@ -68,5 +66,5 @@ index b18f8a2..b0554ea 100644 elif tag == SephTag.SCREEN_DISPLAY_RAW_STATUS: self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") -- -2.26.1 +2.27.0 From d0de7b13266bc1ac36bc47ae5f49b55efd4fc177 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jul 2020 13:06:16 -0400 Subject: [PATCH 140/634] Update bitcoind patch --- test/data/psbt_non_witness_utxo_segwit.patch | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/data/psbt_non_witness_utxo_segwit.patch b/test/data/psbt_non_witness_utxo_segwit.patch index 46719759d..57e41c0b2 100644 --- a/test/data/psbt_non_witness_utxo_segwit.patch +++ b/test/data/psbt_non_witness_utxo_segwit.patch @@ -1,4 +1,4 @@ -From 2789417a7ef31d4b58b2f80783e81a3518dd49af Mon Sep 17 00:00:00 2001 +From eec69a3f42a654a62eacad78164e66b4ae8a68a1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:25 -0400 Subject: [PATCH 1/4] rpc: show both UTXOs in decodepsbt @@ -8,7 +8,7 @@ Subject: [PATCH 1/4] rpc: show both UTXOs in decodepsbt 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp -index e14217c307..45cf6be3a0 100644 +index faec359d1c..5f8c02df65 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1104,6 +1104,7 @@ UniValue decodepsbt(const JSONRPCRequest& request) @@ -45,7 +45,7 @@ index e14217c307..45cf6be3a0 100644 2.27.0 -From 8b7d74fd0e6d3180be0dfb315b4e190f0ad0057d Mon Sep 17 00:00:00 2001 +From 6aa6d5495299a8e09f2e89a71fd5447351fbcf90 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:39 -0400 Subject: [PATCH 2/4] psbt: Allow both non_witness_utxo and witness_utxo @@ -59,7 +59,7 @@ Subject: [PATCH 2/4] psbt: Allow both non_witness_utxo and witness_utxo 5 files changed, 53 deletions(-) diff --git a/src/psbt.cpp b/src/psbt.cpp -index ef9781817a..4c8b40ca0b 100644 +index 10260740f0..71a3e06708 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -35,14 +35,6 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt) @@ -96,7 +96,7 @@ index ef9781817a..4c8b40ca0b 100644 void PSBTOutput::FillSignatureData(SignatureData& sigdata) const { if (!redeem_script.empty()) { -@@ -250,11 +230,6 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& +@@ -261,11 +241,6 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& bool require_witness_sig = false; CTxOut utxo; @@ -108,7 +108,7 @@ index ef9781817a..4c8b40ca0b 100644 if (input.non_witness_utxo) { // If we're taking our information from a non-witness UTXO, verify that it matches the prevout. COutPoint prevout = tx.vin[index].prevout; -@@ -345,10 +320,6 @@ TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector +@@ -356,10 +331,6 @@ TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector return TransactionError::PSBT_MISMATCH; } } @@ -120,7 +120,7 @@ index ef9781817a..4c8b40ca0b 100644 } diff --git a/src/psbt.h b/src/psbt.h -index 888e0fd119..cbf4296bd2 100644 +index 0a8ea2ea0b..401889e2fe 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -62,7 +62,6 @@ struct PSBTInput @@ -179,10 +179,10 @@ index 64328fb66e..908e2b16f2 100644 for (const PSBTOutput& output : psbt.outputs) { diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp -index 8a2a798644..9fae27975d 100644 +index 3cc2611524..38d94335a3 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp -@@ -595,11 +595,6 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb +@@ -597,11 +597,6 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb continue; } @@ -194,7 +194,7 @@ index 8a2a798644..9fae27975d 100644 // Get the Sighash type if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { return TransactionError::SIGHASH_MISMATCH; -@@ -2074,11 +2069,6 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& +@@ -2086,11 +2081,6 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& continue; } @@ -207,10 +207,10 @@ index 8a2a798644..9fae27975d 100644 if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { return TransactionError::SIGHASH_MISMATCH; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp -index 89737ca7b5..054b312cd7 100644 +index 19acfa3322..974985acbf 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp -@@ -2490,11 +2490,6 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp +@@ -2507,11 +2507,6 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp continue; } @@ -226,7 +226,7 @@ index 89737ca7b5..054b312cd7 100644 2.27.0 -From a656eb32d35d2fdc1df5b8d55eba6a0c3efd1c2f Mon Sep 17 00:00:00 2001 +From 889f7b3ce8ba2fccd58ab352ca622e00648147fe Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Jun 2020 23:43:43 -0400 Subject: [PATCH 3/4] psbt: always put a non_witness_utxo and don't remove it @@ -242,7 +242,7 @@ there. 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/psbt.cpp b/src/psbt.cpp -index 4c8b40ca0b..51f829d533 100644 +index 71a3e06708..3fb743e5db 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -136,8 +136,8 @@ void PSBTInput::Merge(const PSBTInput& input) @@ -255,7 +255,7 @@ index 4c8b40ca0b..51f829d533 100644 } partial_sigs.insert(input.partial_sigs.begin(), input.partial_sigs.end()); -@@ -263,10 +263,11 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& +@@ -274,10 +274,11 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& if (require_witness_sig && !sigdata.witness) return false; input.FromSignatureData(sigdata); @@ -270,7 +270,7 @@ index 4c8b40ca0b..51f829d533 100644 // Fill in the missing info diff --git a/src/psbt.h b/src/psbt.h -index cbf4296bd2..275fb03cd8 100644 +index 401889e2fe..0951b76f83 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -67,12 +67,12 @@ struct PSBTInput @@ -289,23 +289,23 @@ index cbf4296bd2..275fb03cd8 100644 SerializeToVector(s, witness_utxo); } diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp -index b4c65a8665..119457aadf 100644 +index 3f85a48ff3..ce7e661b67 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; - std::string final_hex = HexStr(ssTx.begin(), ssTx.end()); + std::string final_hex = HexStr(ssTx); - BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); + BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001008a020000000158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8876500000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); // Mutate the transaction so that one of the inputs is invalid psbtx.tx->vin[0].prevout.n = 2; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp -index 054b312cd7..7816a39ec1 100644 +index 974985acbf..235b269805 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp -@@ -2491,7 +2491,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp +@@ -2508,7 +2508,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp } // If we have no utxo, grab it from the wallet. @@ -315,10 +315,10 @@ index 054b312cd7..7816a39ec1 100644 const auto it = mapWallet.find(txhash); if (it != mapWallet.end()) { diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py -index 9b07c39606..2fe11ef116 100755 +index 660953be9b..7703c4ecb1 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py -@@ -37,6 +37,7 @@ class PSBTTest(BitcoinTestFramework): +@@ -38,6 +38,7 @@ class PSBTTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -326,7 +326,7 @@ index 9b07c39606..2fe11ef116 100755 def test_utxo_conversion(self): mining_node = self.nodes[2] offline_node = self.nodes[0] -@@ -344,7 +345,8 @@ class PSBTTest(BitcoinTestFramework): +@@ -352,7 +353,8 @@ class PSBTTest(BitcoinTestFramework): for i, signer in enumerate(signers): self.nodes[2].unloadwallet("wallet{}".format(i)) @@ -340,7 +340,7 @@ index 9b07c39606..2fe11ef116 100755 2.27.0 -From 836d6fc375ae8709c6175d36f46df365776a497c Mon Sep 17 00:00:00 2001 +From 85d143347a9ef8d1be712b7a699da0f1835dc837 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Jun 2020 19:27:16 -0400 Subject: [PATCH 4/4] tests: Check that segwit inputs in psbt have both UTXO @@ -351,10 +351,10 @@ Subject: [PATCH 4/4] tests: Check that segwit inputs in psbt have both UTXO 1 file changed, 4 insertions(+) diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py -index 2fe11ef116..df7d501e5b 100755 +index 7703c4ecb1..e5e62fd646 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py -@@ -149,6 +149,10 @@ class PSBTTest(BitcoinTestFramework): +@@ -157,6 +157,10 @@ class PSBTTest(BitcoinTestFramework): # spend single key from node 1 rawtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99})['psbt'] walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(rawtx) From b7f3fa67c4264aeef1e985f01631dcff4f5a23c6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 2 Jul 2020 17:00:36 -0400 Subject: [PATCH 141/634] Remove psbt segwit fixes patch to bitcoind This was merged into bitcoind, so no need to patch it ourselves. --- test/data/psbt_non_witness_utxo_segwit.patch | 370 ------------------- test/setup_environment.sh | 1 - 2 files changed, 371 deletions(-) delete mode 100644 test/data/psbt_non_witness_utxo_segwit.patch diff --git a/test/data/psbt_non_witness_utxo_segwit.patch b/test/data/psbt_non_witness_utxo_segwit.patch deleted file mode 100644 index 57e41c0b2..000000000 --- a/test/data/psbt_non_witness_utxo_segwit.patch +++ /dev/null @@ -1,370 +0,0 @@ -From eec69a3f42a654a62eacad78164e66b4ae8a68a1 Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Thu, 4 Jun 2020 23:43:25 -0400 -Subject: [PATCH 1/4] rpc: show both UTXOs in decodepsbt - ---- - src/rpc/rawtransaction.cpp | 9 +++++++-- - 1 file changed, 7 insertions(+), 2 deletions(-) - -diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp -index faec359d1c..5f8c02df65 100644 ---- a/src/rpc/rawtransaction.cpp -+++ b/src/rpc/rawtransaction.cpp -@@ -1104,6 +1104,7 @@ UniValue decodepsbt(const JSONRPCRequest& request) - const PSBTInput& input = psbtx.inputs[i]; - UniValue in(UniValue::VOBJ); - // UTXOs -+ bool have_a_utxo = false; - if (!input.witness_utxo.IsNull()) { - const CTxOut& txout = input.witness_utxo; - -@@ -1121,7 +1122,9 @@ UniValue decodepsbt(const JSONRPCRequest& request) - ScriptToUniv(txout.scriptPubKey, o, true); - out.pushKV("scriptPubKey", o); - in.pushKV("witness_utxo", out); -- } else if (input.non_witness_utxo) { -+ have_a_utxo = true; -+ } -+ if (input.non_witness_utxo) { - UniValue non_wit(UniValue::VOBJ); - TxToUniv(*input.non_witness_utxo, uint256(), non_wit, false); - in.pushKV("non_witness_utxo", non_wit); -@@ -1132,7 +1135,9 @@ UniValue decodepsbt(const JSONRPCRequest& request) - // Hack to just not show fee later - have_all_utxos = false; - } -- } else { -+ have_a_utxo = true; -+ } -+ if (!have_a_utxo) { - have_all_utxos = false; - } - --- -2.27.0 - - -From 6aa6d5495299a8e09f2e89a71fd5447351fbcf90 Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Thu, 4 Jun 2020 23:43:39 -0400 -Subject: [PATCH 2/4] psbt: Allow both non_witness_utxo and witness_utxo - ---- - src/psbt.cpp | 29 ----------------------------- - src/psbt.h | 7 ------- - src/test/fuzz/psbt.cpp | 2 -- - src/wallet/scriptpubkeyman.cpp | 10 ---------- - src/wallet/wallet.cpp | 5 ----- - 5 files changed, 53 deletions(-) - -diff --git a/src/psbt.cpp b/src/psbt.cpp -index 10260740f0..71a3e06708 100644 ---- a/src/psbt.cpp -+++ b/src/psbt.cpp -@@ -35,14 +35,6 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt) - return true; - } - --bool PartiallySignedTransaction::IsSane() const --{ -- for (PSBTInput input : inputs) { -- if (!input.IsSane()) return false; -- } -- return true; --} -- - bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin) - { - if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) { -@@ -158,18 +150,6 @@ void PSBTInput::Merge(const PSBTInput& input) - if (final_script_witness.IsNull() && !input.final_script_witness.IsNull()) final_script_witness = input.final_script_witness; - } - --bool PSBTInput::IsSane() const --{ -- // Cannot have both witness and non-witness utxos -- if (!witness_utxo.IsNull() && non_witness_utxo) return false; -- -- // If we have a witness_script or a scriptWitness, we must also have a witness utxo -- if (!witness_script.empty() && witness_utxo.IsNull()) return false; -- if (!final_script_witness.IsNull() && witness_utxo.IsNull()) return false; -- -- return true; --} -- - void PSBTOutput::FillSignatureData(SignatureData& sigdata) const - { - if (!redeem_script.empty()) { -@@ -261,11 +241,6 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& - bool require_witness_sig = false; - CTxOut utxo; - -- // Verify input sanity, which checks that at most one of witness or non-witness utxos is provided. -- if (!input.IsSane()) { -- return false; -- } -- - if (input.non_witness_utxo) { - // If we're taking our information from a non-witness UTXO, verify that it matches the prevout. - COutPoint prevout = tx.vin[index].prevout; -@@ -356,10 +331,6 @@ TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector - return TransactionError::PSBT_MISMATCH; - } - } -- if (!out.IsSane()) { -- return TransactionError::INVALID_PSBT; -- } -- - return TransactionError::OK; - } - -diff --git a/src/psbt.h b/src/psbt.h -index 0a8ea2ea0b..401889e2fe 100644 ---- a/src/psbt.h -+++ b/src/psbt.h -@@ -62,7 +62,6 @@ struct PSBTInput - void FillSignatureData(SignatureData& sigdata) const; - void FromSignatureData(const SignatureData& sigdata); - void Merge(const PSBTInput& input); -- bool IsSane() const; - PSBTInput() {} - - template -@@ -284,7 +283,6 @@ struct PSBTOutput - void FillSignatureData(SignatureData& sigdata) const; - void FromSignatureData(const SignatureData& sigdata); - void Merge(const PSBTOutput& output); -- bool IsSane() const; - PSBTOutput() {} - - template -@@ -401,7 +399,6 @@ struct PartiallySignedTransaction - /** Merge psbt into this. The two psbts must have the same underlying CTransaction (i.e. the - * same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */ - NODISCARD bool Merge(const PartiallySignedTransaction& psbt); -- bool IsSane() const; - bool AddInput(const CTxIn& txin, PSBTInput& psbtin); - bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout); - PartiallySignedTransaction() {} -@@ -551,10 +548,6 @@ struct PartiallySignedTransaction - if (outputs.size() != tx->vout.size()) { - throw std::ios_base::failure("Outputs provided does not match the number of outputs in transaction."); - } -- // Sanity check -- if (!IsSane()) { -- throw std::ios_base::failure("PSBT is not sane."); -- } - } - - template -diff --git a/src/test/fuzz/psbt.cpp b/src/test/fuzz/psbt.cpp -index 64328fb66e..908e2b16f2 100644 ---- a/src/test/fuzz/psbt.cpp -+++ b/src/test/fuzz/psbt.cpp -@@ -39,7 +39,6 @@ void test_one_input(const std::vector& buffer) - } - - (void)psbt.IsNull(); -- (void)psbt.IsSane(); - - Optional tx = psbt.tx; - if (tx) { -@@ -50,7 +49,6 @@ void test_one_input(const std::vector& buffer) - for (const PSBTInput& input : psbt.inputs) { - (void)PSBTInputSigned(input); - (void)input.IsNull(); -- (void)input.IsSane(); - } - - for (const PSBTOutput& output : psbt.outputs) { -diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp -index 3cc2611524..38d94335a3 100644 ---- a/src/wallet/scriptpubkeyman.cpp -+++ b/src/wallet/scriptpubkeyman.cpp -@@ -597,11 +597,6 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb - continue; - } - -- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. -- if (!input.IsSane()) { -- return TransactionError::INVALID_PSBT; -- } -- - // Get the Sighash type - if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { - return TransactionError::SIGHASH_MISMATCH; -@@ -2086,11 +2081,6 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& - continue; - } - -- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. -- if (!input.IsSane()) { -- return TransactionError::INVALID_PSBT; -- } -- - // Get the Sighash type - if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { - return TransactionError::SIGHASH_MISMATCH; -diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp -index 19acfa3322..974985acbf 100644 ---- a/src/wallet/wallet.cpp -+++ b/src/wallet/wallet.cpp -@@ -2507,11 +2507,6 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp - continue; - } - -- // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. -- if (!input.IsSane()) { -- return TransactionError::INVALID_PSBT; -- } -- - // If we have no utxo, grab it from the wallet. - if (!input.non_witness_utxo && input.witness_utxo.IsNull()) { - const uint256& txhash = txin.prevout.hash; --- -2.27.0 - - -From 889f7b3ce8ba2fccd58ab352ca622e00648147fe Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Thu, 4 Jun 2020 23:43:43 -0400 -Subject: [PATCH 3/4] psbt: always put a non_witness_utxo and don't remove it - -Offline signers will always need a non_witness_utxo so make sure it is -there. ---- - src/psbt.cpp | 7 ++++--- - src/psbt.h | 4 ++-- - src/wallet/test/psbt_wallet_tests.cpp | 2 +- - src/wallet/wallet.cpp | 2 +- - test/functional/rpc_psbt.py | 4 +++- - 5 files changed, 11 insertions(+), 8 deletions(-) - -diff --git a/src/psbt.cpp b/src/psbt.cpp -index 71a3e06708..3fb743e5db 100644 ---- a/src/psbt.cpp -+++ b/src/psbt.cpp -@@ -136,8 +136,8 @@ void PSBTInput::Merge(const PSBTInput& input) - { - if (!non_witness_utxo && input.non_witness_utxo) non_witness_utxo = input.non_witness_utxo; - if (witness_utxo.IsNull() && !input.witness_utxo.IsNull()) { -+ // TODO: For segwit v1, we will want to clear out the non-witness utxo when setting a witness one. For v0 and non-segwit, this is not safe - witness_utxo = input.witness_utxo; -- non_witness_utxo = nullptr; // Clear out any non-witness utxo when we set a witness one. - } - - partial_sigs.insert(input.partial_sigs.begin(), input.partial_sigs.end()); -@@ -274,10 +274,11 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& - if (require_witness_sig && !sigdata.witness) return false; - input.FromSignatureData(sigdata); - -- // If we have a witness signature, use the smaller witness UTXO. -+ // If we have a witness signature, put a witness UTXO. -+ // TODO: For segwit v1, we should remove the non_witness_utxo - if (sigdata.witness) { - input.witness_utxo = utxo; -- input.non_witness_utxo = nullptr; -+ // input.non_witness_utxo = nullptr; - } - - // Fill in the missing info -diff --git a/src/psbt.h b/src/psbt.h -index 401889e2fe..0951b76f83 100644 ---- a/src/psbt.h -+++ b/src/psbt.h -@@ -67,12 +67,12 @@ struct PSBTInput - template - inline void Serialize(Stream& s) const { - // Write the utxo -- // If there is a non-witness utxo, then don't add the witness one. - if (non_witness_utxo) { - SerializeToVector(s, PSBT_IN_NON_WITNESS_UTXO); - OverrideStream os(&s, s.GetType(), s.GetVersion() | SERIALIZE_TRANSACTION_NO_WITNESS); - SerializeToVector(os, non_witness_utxo); -- } else if (!witness_utxo.IsNull()) { -+ } -+ if (!witness_utxo.IsNull()) { - SerializeToVector(s, PSBT_IN_WITNESS_UTXO); - SerializeToVector(s, witness_utxo); - } -diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp -index 3f85a48ff3..ce7e661b67 100644 ---- a/src/wallet/test/psbt_wallet_tests.cpp -+++ b/src/wallet/test/psbt_wallet_tests.cpp -@@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << psbtx; - std::string final_hex = HexStr(ssTx); -- BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); -+ BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001008a020000000158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8876500000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); - - // Mutate the transaction so that one of the inputs is invalid - psbtx.tx->vin[0].prevout.n = 2; -diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp -index 974985acbf..235b269805 100644 ---- a/src/wallet/wallet.cpp -+++ b/src/wallet/wallet.cpp -@@ -2508,7 +2508,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp - } - - // If we have no utxo, grab it from the wallet. -- if (!input.non_witness_utxo && input.witness_utxo.IsNull()) { -+ if (!input.non_witness_utxo) { - const uint256& txhash = txin.prevout.hash; - const auto it = mapWallet.find(txhash); - if (it != mapWallet.end()) { -diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py -index 660953be9b..7703c4ecb1 100755 ---- a/test/functional/rpc_psbt.py -+++ b/test/functional/rpc_psbt.py -@@ -38,6 +38,7 @@ class PSBTTest(BitcoinTestFramework): - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - -+ # TODO: Re-enable this test with segwit v1 - def test_utxo_conversion(self): - mining_node = self.nodes[2] - offline_node = self.nodes[0] -@@ -352,7 +353,8 @@ class PSBTTest(BitcoinTestFramework): - for i, signer in enumerate(signers): - self.nodes[2].unloadwallet("wallet{}".format(i)) - -- self.test_utxo_conversion() -+ # TODO: Re-enable this for segwit v1 -+ # self.test_utxo_conversion() - - # Test that psbts with p2pkh outputs are created properly - p2pkh = self.nodes[0].getnewaddress(address_type='legacy') --- -2.27.0 - - -From 85d143347a9ef8d1be712b7a699da0f1835dc837 Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Mon, 8 Jun 2020 19:27:16 -0400 -Subject: [PATCH 4/4] tests: Check that segwit inputs in psbt have both UTXO - types - ---- - test/functional/rpc_psbt.py | 4 ++++ - 1 file changed, 4 insertions(+) - -diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py -index 7703c4ecb1..e5e62fd646 100755 ---- a/test/functional/rpc_psbt.py -+++ b/test/functional/rpc_psbt.py -@@ -157,6 +157,10 @@ class PSBTTest(BitcoinTestFramework): - # spend single key from node 1 - rawtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99})['psbt'] - walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(rawtx) -+ # Make sure it has both types of UTXOs -+ decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) -+ assert 'non_witness_utxo' in decoded['inputs'][0] -+ assert 'witness_utxo' in decoded['inputs'][0] - assert_equal(walletprocesspsbt_out['complete'], True) - self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) - --- -2.27.0 - diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 6bf0c525a..acb9af26f 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -216,7 +216,6 @@ else bitcoind_setup_needed=true fi fi -git am ../../data/psbt_non_witness_utxo_segwit.patch # Build bitcoind. This is super slow, but it is cached so it runs fairly quickly. if [ "$bitcoind_setup_needed" == true ] ; then From dfb31170535c18cba059c942c5d613b9ca2d0886 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 3 Jul 2020 13:41:43 -0400 Subject: [PATCH 142/634] Update Ledger udev rules From https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules --- hwilib/udev/20-hw1.rules | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/hwilib/udev/20-hw1.rules b/hwilib/udev/20-hw1.rules index 1fd2c66b9..235dee75a 100644 --- a/hwilib/udev/20-hw1.rules +++ b/hwilib/udev/20-hw1.rules @@ -1,9 +1,12 @@ -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="2b7c", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="3b7c", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="4b7c", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1807", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1808", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev" -SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004", MODE="0660", GROUP="plugdev" \ No newline at end of file +# HW.1 / Nano +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl" +# Blue +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009|000a|000b|000c|000d|000e|000f|0010|0011|0012|0013|0014|0015|0016|0017|0018|0019|001a|001b|001c|001d|001e|001f", TAG+="uaccess", TAG+="udev-acl" +# Nano S +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001|1000|1001|1002|1003|1004|1005|1006|1007|1008|1009|100a|100b|100c|100d|100e|100f|1010|1011|1012|1013|1014|1015|1016|1017|1018|1019|101a|101b|101c|101d|101e|101f", TAG+="uaccess", TAG+="udev-acl" +# Aramis +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0002|2000|2001|2002|2003|2004|2005|2006|2007|2008|2009|200a|200b|200c|200d|200e|200f|2010|2011|2012|2013|2014|2015|2016|2017|2018|2019|201a|201b|201c|201d|201e|201f", TAG+="uaccess", TAG+="udev-acl" +# HW2 +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|3002|3003|3004|3005|3006|3007|3008|3009|300a|300b|300c|300d|300e|300f|3010|3011|3012|3013|3014|3015|3016|3017|3018|3019|301a|301b|301c|301d|301e|301f", TAG+="uaccess", TAG+="udev-acl" +# Nano X +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl" From c72cb4fdb469b6f55337772dbc76dd7663e05882 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 21 Jul 2020 13:06:50 +0200 Subject: [PATCH 143/634] psbt: prevout hash check always failed `input.non_witness_utxo.rehash()` returns None, so the if clause was never entered. Also, prevout.sha256 does not exist. --- hwilib/serializations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 968088aa4..4c25fba59 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -792,8 +792,10 @@ def deserialize(self, psbt): input.deserialize(f) self.inputs.append(input) - if input.non_witness_utxo and input.non_witness_utxo.rehash() and input.non_witness_utxo.sha256 != txin.prevout.sha256: - raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash") + if input.non_witness_utxo: + input.non_witness_utxo.rehash() + if input.non_witness_utxo.sha256 != txin.prevout.hash: + raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash") if (len(self.inputs) != len(self.tx.vin)): raise PSBTSerializationError("Inputs provided does not match the number of inputs in transaction") From 678fa5cbcfd25d30c8a4b9e0ac5ef10ec6090245 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 21 Jul 2020 12:47:47 +0200 Subject: [PATCH 144/634] serialization: remove unneeded hex->unhex Going from base64 to bytes does not need an intermediate hex->unhex step. --- hwilib/serializations.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 4c25fba59..f60d49614 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -131,9 +131,6 @@ def ser_string_vector(v): r += ser_string(sv) return r -def Base64ToHex(s): - return binascii.hexlify(base64.b64decode(s)) - def HexToBase64(s): return base64.b64encode(binascii.unhexlify(s)) @@ -733,9 +730,9 @@ def __init__(self, tx=None): self.unknown = {} def deserialize(self, psbt): - hexstring = Base64ToHex(psbt.strip()) - f = BufferedReader(BytesIO(binascii.unhexlify(hexstring))) - end = len(binascii.unhexlify(hexstring)) + psbt_bytes = base64.b64decode(psbt.strip()) + f = BufferedReader(BytesIO(psbt_bytes)) + end = len(psbt_bytes) # Read the magic bytes magic = f.read(5) From c2d04d64713c1ea375f44f54bd159ab4163cce69 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 21 Jul 2020 12:45:01 +0200 Subject: [PATCH 145/634] serializations: remove unused arg --- hwilib/serializations.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index f60d49614..7a74dae06 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -103,16 +103,10 @@ def deser_vector(f, c): return r -# ser_function_name: Allow for an alternate serialization function on the -# entries in the vector (we use this for serializing the vector of transactions -# for a witness block). -def ser_vector(v, ser_function_name=None): +def ser_vector(v): r = ser_compact_size(len(v)) for i in v: - if ser_function_name: - r += getattr(i, ser_function_name)() - else: - r += i.serialize() + r += i.serialize() return r From 2580b66de5f2c7cbe4a88c3af89e747a364f8289 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 21 Jul 2020 12:37:12 +0200 Subject: [PATCH 146/634] hwilib: add more type annotations Makes this pass without errors: mypy --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py --- .travis.yml | 2 +- hwilib/base58.py | 9 +- hwilib/errors.py | 42 +++--- hwilib/serializations.py | 278 ++++++++++++++++++++++----------------- 4 files changed, 187 insertions(+), 144 deletions(-) diff --git a/.travis.yml b/.travis.yml index 225045f45..33e9d8113 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ jobs: stage: lint install: - pip install mypy - script: mypy hwilib/base58.py + script: mypy --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py - name: Run non-device tests only stage: test install: diff --git a/hwilib/base58.py b/hwilib/base58.py index 27bddafb3..03d384bf9 100644 --- a/hwilib/base58.py +++ b/hwilib/base58.py @@ -14,10 +14,10 @@ from typing import List b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -def sha256(s): +def sha256(s: bytes) -> bytes: return hashlib.new('sha256', s).digest() -def hash256(s): +def hash256(s: bytes) -> bytes: return sha256(sha256(s)) def encode(b: bytes) -> str: @@ -72,10 +72,11 @@ def decode(s: str) -> bytes: break return b'\x00' * pad + res -def get_xpub_fingerprint(s: str) -> str: +def get_xpub_fingerprint(s: str) -> int: data = decode(s) fingerprint = data[5:9] - return struct.unpack(" str: data = decode(xpub) diff --git a/hwilib/errors.py b/hwilib/errors.py index 6240c99e6..f7ac8f61b 100644 --- a/hwilib/errors.py +++ b/hwilib/errors.py @@ -1,5 +1,6 @@ # Defines errors and error codes +from typing import Any, Dict, Iterator, Optional from contextlib import contextmanager # Error codes @@ -24,74 +25,79 @@ # Exceptions class HWWError(Exception): - def __init__(self, msg, code): + def __init__(self, msg: str, code: int): Exception.__init__(self) self.code = code self.msg = msg - def get_code(self): + def get_code(self) -> int: return self.code - def get_msg(self): + def get_msg(self) -> str: return self.msg - def __str__(self): + def __str__(self) -> str: return self.msg class NoPasswordError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, NO_PASSWORD) class UnavailableActionError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, UNAVAILABLE_ACTION) class DeviceAlreadyInitError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_ALREADY_INIT) class DeviceNotReadyError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_NOT_READY) class DeviceAlreadyUnlockedError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_ALREADY_UNLOCKED) class UnknownDeviceError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, UNKNWON_DEVICE_TYPE) class NotImplementedError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, NOT_IMPLEMENTED) class PSBTSerializationError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, INVALID_TX) class BadArgumentError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, BAD_ARGUMENT) class DeviceFailureError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, UNKNOWN_ERROR) class ActionCanceledError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, ACTION_CANCELED) class DeviceConnectionError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_CONN_ERROR) class DeviceBusyError(HWWError): - def __init__(self, msg): + def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_BUSY) @contextmanager -def handle_errors(msg=None, result=None, code=UNKNOWN_ERROR, debug=False): +def handle_errors( + msg: Optional[str] = None, + result: Optional[Dict[str, Any]] = None, + code: int = UNKNOWN_ERROR, + debug: bool = False, +) -> Iterator[None]: if result is None: result = {} diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 7a74dae06..eee50033c 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -16,7 +16,6 @@ """ from io import BytesIO, BufferedReader -from codecs import encode from .errors import PSBTSerializationError from . import base58 @@ -25,22 +24,47 @@ import hashlib import copy import base64 - -def sha256(s): +from typing import ( + Dict, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + TypeVar, + Callable, +) + +from typing_extensions import Protocol + +class Readable(Protocol): + def read(self, n: int = -1) -> bytes: + ... + +class Deserializable(Protocol): + def deserialize(self, f: Readable) -> None: + ... + +class Serializable(Protocol): + def serialize(self) -> bytes: + ... + +def sha256(s: bytes) -> bytes: return hashlib.new('sha256', s).digest() -def ripemd160(s): +def ripemd160(s: bytes) -> bytes: return hashlib.new('ripemd160', s).digest() -def hash256(s): +def hash256(s: bytes) -> bytes: return sha256(sha256(s)) -def hash160(s): +def hash160(s: bytes) -> bytes: return ripemd160(sha256(s)) # Serialization/deserialization tools -def ser_compact_size(size): +def ser_compact_size(size: int) -> bytes: r = b"" if size < 253: r = struct.pack("B", size) @@ -52,8 +76,8 @@ def ser_compact_size(size): r = struct.pack(" int: + nit: int = struct.unpack(" bytes: nit = deser_compact_size(f) return f.read(nit) -def ser_string(s): +def ser_string(s: bytes) -> bytes: return ser_compact_size(len(s)) + s -def deser_uint256(f): +def deser_uint256(f: Readable) -> int: r = 0 for i in range(8): t = struct.unpack(" bytes: rs = b"" for i in range(8): rs += struct.pack(" int: r = 0 t = struct.unpack(" List[D]: nit = deser_compact_size(f) r = [] for i in range(nit): @@ -103,14 +128,14 @@ def deser_vector(f, c): return r -def ser_vector(v): +def ser_vector(v: Sequence[Serializable]) -> bytes: r = ser_compact_size(len(v)) for i in v: r += i.serialize() return r -def deser_string_vector(f): +def deser_string_vector(f: Readable) -> List[bytes]: nit = deser_compact_size(f) r = [] for i in range(nit): @@ -119,16 +144,16 @@ def deser_string_vector(f): return r -def ser_string_vector(v): +def ser_string_vector(v: List[bytes]) -> bytes: r = ser_compact_size(len(v)) for sv in v: r += ser_string(sv) return r -def HexToBase64(s): +def HexToBase64(s: str) -> bytes: return base64.b64encode(binascii.unhexlify(s)) -def ser_sig_der(r, s): +def ser_sig_der(r: bytes, s: bytes) -> bytes: sig = b"\x30" # Make r and s as short as possible @@ -172,7 +197,7 @@ def ser_sig_der(r, s): sig += b"\x01" return sig -def ser_sig_compact(r, s, recid): +def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: rec = struct.unpack("B", recid)[0] prefix = struct.pack("B", 27 + 4 + rec) @@ -187,26 +212,31 @@ def ser_sig_compact(r, s, recid): MSG_WITNESS_FLAG = 1 << 30 class COutPoint(object): - def __init__(self, hash=0, n=0xffffffff): + def __init__(self, hash: int = 0, n: int = 0xffffffff): self.hash = hash self.n = n - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: self.hash = deser_uint256(f) self.n = struct.unpack(" bytes: r = b"" r += ser_uint256(self.hash) r += struct.pack(" str: return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) class CTxIn(object): - def __init__(self, outpoint=None, scriptSig=b"", nSequence=0): + def __init__( + self, + outpoint: Optional[COutPoint] = None, + scriptSig: bytes = b"", + nSequence: int = 0, + ): if outpoint is None: self.prevout = COutPoint() else: @@ -214,45 +244,47 @@ def __init__(self, outpoint=None, scriptSig=b"", nSequence=0): self.scriptSig = scriptSig self.nSequence = nSequence - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: self.prevout = COutPoint() self.prevout.deserialize(f) self.scriptSig = deser_string(f) self.nSequence = struct.unpack(" bytes: r = b"" r += self.prevout.serialize() r += ser_string(self.scriptSig) r += struct.pack(" str: return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ % (repr(self.prevout), self.scriptSig.hex(), self.nSequence) -def is_p2sh(script): +def is_p2sh(script: bytes) -> bool: return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 -def is_p2pkh(script): +def is_p2pkh(script: bytes) -> bool: return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac -def is_p2pk(script): +def is_p2pk(script: bytes) -> bool: return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac -def is_witness(script): +def is_witness(script: bytes) -> Tuple[bool, int, bytes]: if len(script) < 4 or len(script) > 42: - return (False, None, None) + return (False, 0, b"") if script[0] != 0 and (script[0] < 81 or script[0] > 96): - return (False, None, None) + return (False, 0, b"") if script[1] + 2 == len(script): return (True, script[0] - 0x50 if script[0] else 0, script[2:]) -def is_p2wpkh(script): + return (False, 0, b"") + +def is_p2wpkh(script: bytes) -> bool: is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False @@ -260,7 +292,7 @@ def is_p2wpkh(script): return False return len(wit_prog) == 20 -def is_p2wsh(script): +def is_p2wsh(script: bytes) -> bool: is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False @@ -270,78 +302,78 @@ def is_p2wsh(script): class CTxOut(object): - def __init__(self, nValue=0, scriptPubKey=b""): + def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): self.nValue = nValue self.scriptPubKey = scriptPubKey - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: self.nValue = struct.unpack(" bytes: r = b"" r += struct.pack(" bool: return is_p2sh(self.scriptPubKey) - def is_p2pkh(self): + def is_p2pkh(self) -> bool: return is_p2pkh(self.scriptPubKey) - def is_p2pk(self): + def is_p2pk(self) -> bool: return is_p2pk(self.scriptPubKey) - def is_witness(self): + def is_witness(self) -> Tuple[bool, int, bytes]: return is_witness(self.scriptPubKey) - def __repr__(self): + def __repr__(self) -> str: return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ - % (self.nValue, self.nValue, binascii.hexlify(self.scriptPubKey)) + % (self.nValue, self.nValue, self.scriptPubKey.hex()) class CScriptWitness(object): - def __init__(self): + def __init__(self) -> None: # stack is a vector of strings - self.stack = [] + self.stack: List[bytes] = [] - def __repr__(self): + def __repr__(self) -> str: return "CScriptWitness(%s)" % \ (",".join([x.hex() for x in self.stack])) - def is_null(self): + def is_null(self) -> bool: if self.stack: return False return True class CTxInWitness(object): - def __init__(self): + def __init__(self) -> None: self.scriptWitness = CScriptWitness() - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: self.scriptWitness.stack = deser_string_vector(f) - def serialize(self): + def serialize(self) -> bytes: return ser_string_vector(self.scriptWitness.stack) - def __repr__(self): + def __repr__(self) -> str: return repr(self.scriptWitness) - def is_null(self): + def is_null(self) -> bool: return self.scriptWitness.is_null() class CTxWitness(object): - def __init__(self): - self.vtxinwit = [] + def __init__(self) -> None: + self.vtxinwit: List[CTxInWitness] = [] - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: for i in range(len(self.vtxinwit)): self.vtxinwit[i].deserialize(f) - def serialize(self): + def serialize(self) -> bytes: r = b"" # This is different than the usual vector serialization -- # we omit the length of the vector, which is required to be @@ -350,11 +382,11 @@ def serialize(self): r += x.serialize() return r - def __repr__(self): + def __repr__(self) -> str: return "CTxWitness(%s)" % \ (';'.join([repr(x) for x in self.vtxinwit])) - def is_null(self): + def is_null(self) -> bool: for x in self.vtxinwit: if not x.is_null(): return False @@ -362,15 +394,15 @@ def is_null(self): class CTransaction(object): - def __init__(self, tx=None): + def __init__(self, tx: Optional['CTransaction'] = None) -> None: if tx is None: self.nVersion = 1 - self.vin = [] - self.vout = [] + self.vin: List[CTxIn] = [] + self.vout: List[CTxOut] = [] self.wit = CTxWitness() self.nLockTime = 0 - self.sha256 = None - self.hash = None + self.sha256: Optional[int] = None + self.hash: Optional[str] = None else: self.nVersion = tx.nVersion self.vin = copy.deepcopy(tx.vin) @@ -380,7 +412,7 @@ def __init__(self, tx=None): self.hash = tx.hash self.wit = copy.deepcopy(tx.wit) - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: self.nVersion = struct.unpack(" bytes: r = b"" r += struct.pack(" bytes: flags = 0 if not self.wit.is_null(): flags |= 1 r = b"" r += struct.pack(" bytes: return self.serialize_without_witness() # Recalculate the txid (transaction hash without witness) - def rehash(self): + def rehash(self) -> None: self.sha256 = None self.calc_sha256() # We will only cache the serialization without witness in # self.sha256 and self.hash -- those are expected to be the txid. - def calc_sha256(self, with_witness=False): + def calc_sha256(self, with_witness: bool = False) -> Optional[int]: if with_witness: # Don't cache the result, just return it return uint256_from_str(hash256(self.serialize_with_witness())) if self.sha256 is None: self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) - self.hash = encode(hash256(self.serialize())[::-1], 'hex_codec').decode('ascii') + self.hash = hash256(self.serialize())[::-1].hex() + return None - def is_null(self): + def is_null(self) -> bool: return len(self.vin) == 0 and len(self.vout) == 0 - def __repr__(self): + def __repr__(self) -> str: return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) -def DeserializeHDKeypath(f, key, hd_keypaths): +def DeserializeHDKeypath( + f: Readable, + key: bytes, + hd_keypaths: MutableMapping[bytes, Sequence[int]], +) -> None: if len(key) != 34 and len(key) != 66: raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") pubkey = key[1:] @@ -467,9 +503,9 @@ def DeserializeHDKeypath(f, key, hd_keypaths): raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") value = deser_string(f) - hd_keypaths[pubkey] = struct.unpack("<" + "I" * (len(value) // 4), value) + hd_keypaths[pubkey] = list(struct.unpack("<" + "I" * (len(value) // 4), value)) -def SerializeHDKeypath(hd_keypaths, type): +def SerializeHDKeypath(hd_keypaths: Mapping[bytes, Sequence[int]], type: bytes) -> bytes: r = b"" for pubkey, path in sorted(hd_keypaths.items()): r += ser_string(type + pubkey) @@ -478,19 +514,19 @@ def SerializeHDKeypath(hd_keypaths, type): return r class PartiallySignedInput: - def __init__(self): - self.non_witness_utxo = None - self.witness_utxo = None - self.partial_sigs = {} + def __init__(self) -> None: + self.non_witness_utxo: Optional[CTransaction] = None + self.witness_utxo: Optional[CTxOut] = None + self.partial_sigs: Dict[bytes, bytes] = {} self.sighash = 0 self.redeem_script = b"" self.witness_script = b"" - self.hd_keypaths = {} + self.hd_keypaths: Dict[bytes, Sequence[int]] = {} self.final_script_sig = b"" self.final_script_witness = CTxInWitness() - self.unknown = {} + self.unknown: Dict[bytes, bytes] = {} - def set_null(self): + def set_null(self) -> None: self.non_witness_utxo = None self.witness_utxo = None self.partial_sigs.clear() @@ -502,7 +538,7 @@ def set_null(self): self.final_script_witness = CTxInWitness() self.unknown.clear() - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: while True: # read the key try: @@ -523,8 +559,8 @@ def deserialize(self, f): elif len(key) != 1: raise PSBTSerializationError("non witness utxo key is more than one byte type") self.non_witness_utxo = CTransaction() - value = BufferedReader(BytesIO(deser_string(f))) - self.non_witness_utxo.deserialize(value) + utxo_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.non_witness_utxo.deserialize(utxo_bytes) self.non_witness_utxo.rehash() elif key_type == 1: @@ -533,8 +569,8 @@ def deserialize(self, f): elif len(key) != 1: raise PSBTSerializationError("witness utxo key is more than one byte type") self.witness_utxo = CTxOut() - value = BufferedReader(BytesIO(deser_string(f))) - self.witness_utxo.deserialize(value) + tx_out_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.witness_utxo.deserialize(tx_out_bytes) elif key_type == 2: if len(key) != 34 and len(key) != 66: @@ -551,8 +587,8 @@ def deserialize(self, f): raise PSBTSerializationError("Duplicate key, input sighash type already provided") elif len(key) != 1: raise PSBTSerializationError("sighash key is more than one byte type") - value = deser_string(f) - self.sighash = struct.unpack(" bytes: r = b"" if self.non_witness_utxo: @@ -642,19 +678,19 @@ def serialize(self): return r class PartiallySignedOutput: - def __init__(self): + def __init__(self) -> None: self.redeem_script = b"" self.witness_script = b"" - self.hd_keypaths = {} - self.unknown = {} + self.hd_keypaths: Dict[bytes, Sequence[int]] = {} + self.unknown: Dict[bytes, bytes] = {} - def set_null(self): + def set_null(self) -> None: self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths.clear() self.unknown.clear() - def deserialize(self, f): + def deserialize(self, f: Readable) -> None: while True: # read the key try: @@ -692,7 +728,7 @@ def deserialize(self, f): value = deser_string(f) self.unknown[key] = value - def serialize(self): + def serialize(self) -> bytes: r = b"" if len(self.redeem_script) != 0: r += ser_string(b"\x00") @@ -714,18 +750,18 @@ def serialize(self): class PSBT(object): - def __init__(self, tx=None): + def __init__(self, tx: Optional[CTransaction] = None) -> None: if tx: self.tx = tx else: self.tx = CTransaction() - self.inputs = [] - self.outputs = [] - self.unknown = {} + self.inputs: List[PartiallySignedInput] = [] + self.outputs: List[PartiallySignedOutput] = [] + self.unknown: Dict[bytes, bytes] = {} - def deserialize(self, psbt): + def deserialize(self, psbt: str) -> None: psbt_bytes = base64.b64decode(psbt.strip()) - f = BufferedReader(BytesIO(psbt_bytes)) + f = BufferedReader(BytesIO(psbt_bytes)) # type: ignore end = len(psbt_bytes) # Read the magic bytes @@ -757,8 +793,8 @@ def deserialize(self, psbt): raise PSBTSerializationError("Global unsigned tx key is more than one byte type") # read in value - value = BufferedReader(BytesIO(deser_string(f))) - self.tx.deserialize(value) + tx_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.tx.deserialize(tx_bytes) # Make sure that all scriptSigs and scriptWitnesses are empty for txin in self.tx.vin: @@ -768,8 +804,8 @@ def deserialize(self, psbt): else: if key in self.unknown: raise PSBTSerializationError("Duplicate key, key for unknown value already provided") - value = deser_string(f) - self.unknown[key] = value + unknown_bytes = deser_string(f) + self.unknown[key] = unknown_bytes # make sure that we got an unsigned tx if self.tx.is_null(): @@ -802,7 +838,7 @@ def deserialize(self, psbt): if len(self.outputs) != len(self.tx.vout): raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") - def serialize(self): + def serialize(self) -> str: r = b"" # magic bytes @@ -833,7 +869,7 @@ def serialize(self): r += output.serialize() # return hex string - return HexToBase64(binascii.hexlify(r)).decode() + return HexToBase64(r.hex()).decode() # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else @@ -844,7 +880,7 @@ class ExtendedKey(object): TESTNET_PUBLIC = b'\x04\x35\x87\xCF' TESTNET_PRIVATE = b'\x04\x35\x83\x94' - def __init__(self): + def __init__(self) -> None: self.is_testnet = False self.is_private = False self.depth = 0 @@ -854,7 +890,7 @@ def __init__(self): self.pubkey = b'' self.privkey = b'' - def deserialize(self, xpub: str): + def deserialize(self, xpub: str) -> None: data = base58.decode(xpub)[:-4] # Decoded xpub without checksum version = data[0:4] @@ -873,8 +909,8 @@ def deserialize(self, xpub: str): else: self.pubkey = data[45:78] - def get_printable_dict(self): - d = {} + def get_printable_dict(self) -> Dict[str, object]: + d: Dict[str, object] = {} d['testnet'] = self.is_testnet d['private'] = self.is_private d['depth'] = self.depth From 6ad1f8924fa0cb734902747ff755c2cfcd90af0b Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 22 Jul 2020 00:54:31 +0200 Subject: [PATCH 147/634] fix CI issues --- test/test_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index de033fe1e..ad245237e 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -304,9 +304,9 @@ def _generate_and_finalize(self, unknown_inputs, psbt): # Single input PSBTs will be fully signed by first signer for psbt_input in first_psbt.inputs[1:]: for pubkey, path in psbt_input.hd_keypaths.items(): - psbt_input.hd_keypaths[pubkey] = (0,) + path[1:] + psbt_input.hd_keypaths[pubkey] = [0] + path[1:] for pubkey, path in second_psbt.inputs[0].hd_keypaths.items(): - second_psbt.inputs[0].hd_keypaths[pubkey] = (0,) + path[1:] + second_psbt.inputs[0].hd_keypaths[pubkey] = [0] + path[1:] single_input = len(first_psbt.inputs) == 1 From a78f8d68f61b0ab23e616b03765129656744b54e Mon Sep 17 00:00:00 2001 From: Kevin Mulcrone Date: Wed, 22 Jul 2020 23:50:39 -0600 Subject: [PATCH 148/634] add not is_wit check to trezor --- hwilib/devices/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 450fcfb90..bb1b69af1 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -257,7 +257,7 @@ def ignore_input(): if is_ms: # Add to txinputtype txinputtype.multisig = multisig - if not psbt_in.witness_utxo: + if not is_wit: if utxo.is_p2sh: txinputtype.script_type = proto.InputScriptType.SPENDMULTISIG else: From 67a9d7a322f35b3685ed48bede3c8990da116460 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 7 Aug 2020 23:59:48 +0300 Subject: [PATCH 149/634] Fix descriptor fingerprint parsing when no derivation used --- hwilib/descriptor.py | 3 +++ test/test_descriptor.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 85f4fddd2..ade8d2b12 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -147,6 +147,9 @@ def parse(cls, desc, testnet=False): origin_path = match.group(2) # Replace h with ' origin_path = origin_path.replace('h', '\'') + else: + origin_fingerprint = origin + origin_path = '' base_key_and_path_match = re.search(r"\[.*\](\w+)([\d'\/\*]*)", key) else: diff --git a/test/test_descriptor.py b/test/test_descriptor.py index a479c5962..abdfe5fc9 100755 --- a/test/test_descriptor.py +++ b/test/test_descriptor.py @@ -28,6 +28,18 @@ def test_parse_descriptor_without_origin(self): self.assertEqual(desc.testnet, True) self.assertEqual(desc.m_path, None) + def test_parse_descriptor_with_origin_fingerprint_only(self): + desc = Descriptor.parse("wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) + self.assertIsNotNone(desc) + self.assertEqual(desc.wpkh, True) + self.assertEqual(desc.sh_wpkh, None) + self.assertEqual(desc.origin_fingerprint, "00000001") + self.assertEqual(desc.origin_path, "") + self.assertEqual(desc.base_key, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.path_suffix, "/0/0") + self.assertEqual(desc.testnet, True) + self.assertEqual(desc.m_path, None) + def test_parse_descriptor_with_key_at_end_with_origin(self): desc = Descriptor.parse("wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)", True) self.assertIsNotNone(desc) From d16bdf794f7090a5c7015c1e3b3398440df8a440 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Thu, 6 Aug 2020 17:22:01 +0200 Subject: [PATCH 150/634] extended sign_message to also sign bytes, not only string --- hwilib/devices/coldcard.py | 13 +++++++++---- hwilib/devices/digitalbitbox.py | 10 ++++++---- hwilib/devices/ledger.py | 11 +++++++---- hwilib/devices/trezor.py | 6 +++--- hwilib/devices/trezorlib/btc.py | 10 ++++++++-- hwilib/devices/trezorlib/tools.py | 6 +++--- hwilib/hwwclient.py | 8 ++++++-- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 7848d1bab..03d71b85b 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -1,5 +1,7 @@ # Coldcard interaction script +from typing import Dict, Union + from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -174,15 +176,18 @@ def sign_tx(self, tx): tx.deserialize(base64.b64encode(result).decode()) return {'psbt': tx.serialize()} - # Must return a base64 encoded string with the signed message - # The message can be any string. keypath is the bip 32 derivation path for the key to sign with @coldcard_exception - def sign_message(self, message, keypath): + def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') - ok = self.device.send_recv(CCProtocolPacker.sign_message(message.encode(), keypath, AF_CLASSIC), timeout=None) + msg = message + if not isinstance(message, bytes): + msg = message.encode() + ok = self.device.send_recv( + CCProtocolPacker.sign_message(msg, keypath, AF_CLASSIC), timeout=None + ) assert ok is None if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 5ff6d7fce..63b52466b 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -13,6 +13,7 @@ import socket import sys import time +from typing import Dict, Union from ..hwwclient import HardwareWalletClient from ..errors import ( @@ -512,14 +513,15 @@ def sign_tx(self, tx): return {'psbt': tx.serialize()} - # Must return a base64 encoded string with the signed message - # The message can be any string @digitalbitbox_exception - def sign_message(self, message, keypath): + def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: to_hash = b"" to_hash += self.message_magic to_hash += ser_compact_size(len(message)) - to_hash += message.encode() + if isinstance(message, bytes): + to_hash += message + else: + to_hash += message.encode() hashed_message = hash256(to_hash) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 0295de09f..2ba9cad82 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,5 +1,7 @@ # Ledger interaction script +from typing import Dict, Union + from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -311,13 +313,14 @@ def sign_tx(self, tx): # Send PSBT back return {'psbt': tx.serialize()} - # Must return a base64 encoded string with the signed message - # The message can be any string @ledger_exception - def sign_message(self, message, keypath): + def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") - message = bytearray(message, 'utf-8') + if isinstance(message, str): + message = bytearray(message, 'utf-8') + else: + message = bytearray(message) keypath = keypath[2:] # First display on screen what address you're signing for self.app.getWalletPublicKey(keypath, True) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index bb1b69af1..a5c4b779a 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,5 +1,7 @@ # Trezor interaction script +from typing import Dict, Union + from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -399,10 +401,8 @@ def ignore_input(): return {'psbt': tx.serialize()} - # Must return a base64 encoded string with the signed message - # The message can be any string @trezor_exception - def sign_message(self, message, keypath): + def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: self._check_unlocked() path = tools.parse_path(keypath) result = btc.sign_message(self.client, self.coin_name, path, message) diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index c0059e829..7e97758ea 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -15,9 +15,10 @@ # If not, see . import binascii +from typing import Union from . import messages -from .tools import CallException, expect, normalize_nfc, session +from .tools import expect, normalize_nfc, session @expect(messages.PublicKey) @@ -62,7 +63,11 @@ def get_address( @expect(messages.MessageSignature) def sign_message( - client, coin_name, n, message, script_type=messages.InputScriptType.SPENDADDRESS + client, + coin_name, + n, + message: Union[str, bytes], + script_type=messages.InputScriptType.SPENDADDRESS, ): message = normalize_nfc(message) return client.call( @@ -71,6 +76,7 @@ def sign_message( ) ) + @session def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None): this_tx = messages.TransactionType(inputs=inputs, outputs=outputs) diff --git a/hwilib/devices/trezorlib/tools.py b/hwilib/devices/trezorlib/tools.py index dca461e0b..83552b5f6 100644 --- a/hwilib/devices/trezorlib/tools.py +++ b/hwilib/devices/trezorlib/tools.py @@ -19,7 +19,7 @@ import re import struct import unicodedata -from typing import List, NewType +from typing import List, NewType, Union from .exceptions import TrezorFailure @@ -181,13 +181,13 @@ def str_to_harden(x: str) -> int: raise ValueError("Invalid BIP32 path", nstr) -def normalize_nfc(txt): +def normalize_nfc(txt: Union[str, bytes]) -> bytes: """ Normalize message to NFC and return bytes suitable for protobuf. This seems to be bitcoin-qt standard of doing things. """ if isinstance(txt, bytes): - txt = txt.decode() + return txt return unicodedata.normalize("NFC", txt).encode() diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index bafc84c46..77583759a 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -55,10 +55,14 @@ def sign_tx(self, psbt: PSBT) -> Dict[str, str]: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def sign_message(self, message: str, bip32_path: str) -> Dict[str, str]: + def sign_message( + self, message: Union[str, bytes], bip32_path: str + ) -> Dict[str, str]: """Sign a message (bitcoin message signing). - Sign the message according to the bitcoin message signing standard. + Sign the message according to the bitcoin message signing standard: + usually, the message is a string that is encoded to bytes; + anyway, if the message is already bytes it is processed untouched. Retrieve the signing key at the specified BIP32 derivation path. From 56f38aae8d3bed57005eb38f93c5e0b3a02c6442 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Sat, 8 Aug 2020 16:35:52 +0200 Subject: [PATCH 151/634] avoided 'unused variables' namespace pollution --- hwilib/devices/coldcard.py | 4 ++-- hwilib/serializations.py | 8 ++++---- test/test_udevrules.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 7848d1bab..99ef7cdd3 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -122,7 +122,7 @@ def sign_tx(self, tx): if our_keys > passes: passes = our_keys - for i in range(0, passes): + for _ in range(passes): # Get psbt in hex and then make binary fd = io.BytesIO(base64.b64decode(tx.serialize())) @@ -198,7 +198,7 @@ def sign_message(self, message, keypath): if len(done) != 2: raise DeviceFailureError('Failed: %r' % done) - addr, raw = done + _, raw = done sig = str(base64.b64encode(raw), 'ascii').replace('\n', '') return {"signature": sig} diff --git a/hwilib/serializations.py b/hwilib/serializations.py index eee50033c..228558520 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -103,7 +103,7 @@ def deser_uint256(f: Readable) -> int: def ser_uint256(u: int) -> bytes: rs = b"" - for i in range(8): + for _ in range(8): rs += struct.pack(">= 32 return rs @@ -121,7 +121,7 @@ def uint256_from_str(s: bytes) -> int: def deser_vector(f: Readable, c: Callable[[], D]) -> List[D]: nit = deser_compact_size(f) r = [] - for i in range(nit): + for _ in range(nit): t = c() t.deserialize(f) r.append(t) @@ -138,7 +138,7 @@ def ser_vector(v: Sequence[Serializable]) -> bytes: def deser_string_vector(f: Readable) -> List[bytes]: nit = deser_compact_size(f) r = [] - for i in range(nit): + for _ in range(nit): t = deser_string(f) r.append(t) return r @@ -456,7 +456,7 @@ def serialize_with_witness(self) -> bytes: if (len(self.wit.vtxinwit) != len(self.vin)): # vtxinwit must have the same length as vin self.wit.vtxinwit = self.wit.vtxinwit[:len(self.vin)] - for i in range(len(self.wit.vtxinwit), len(self.vin)): + for _ in range(len(self.wit.vtxinwit), len(self.vin)): self.wit.vtxinwit.append(CTxInWitness()) r += self.wit.serialize() r += struct.pack(" Date: Sat, 8 Aug 2020 16:46:45 +0200 Subject: [PATCH 152/634] removed unused variables in the devices folder codebase --- hwilib/devices/btchip/bitcoinTransaction.py | 4 ++-- hwilib/devices/ckcc/client.py | 4 ++-- hwilib/devices/ckcc/utils.py | 4 ++-- hwilib/devices/trezorlib/protobuf.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hwilib/devices/btchip/bitcoinTransaction.py b/hwilib/devices/btchip/bitcoinTransaction.py index 35276e9e1..9543c7abc 100644 --- a/hwilib/devices/btchip/bitcoinTransaction.py +++ b/hwilib/devices/btchip/bitcoinTransaction.py @@ -101,14 +101,14 @@ def __init__(self, data=None): inputSize = readVarint(data, offset) offset += inputSize['size'] numInputs = inputSize['value'] - for i in range(numInputs): + for _ in range(numInputs): tmp = { 'buffer': data, 'offset' : offset} self.inputs.append(bitcoinInput(tmp)) offset = tmp['offset'] outputSize = readVarint(data, offset) offset += outputSize['size'] numOutputs = outputSize['value'] - for i in range(numOutputs): + for _ in range(numOutputs): tmp = { 'buffer': data, 'offset' : offset} self.outputs.append(bitcoinOutput(tmp)) offset = tmp['offset'] diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index 3159cdbda..f170e7347 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -159,7 +159,7 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=1000, encrypt=T print("Rx [%2d]: %r" % (len(resp), b2a_hex(bytes(resp)))) return CCProtocolUnpacker.decode(resp) - except CCProtoError as e: + except CCProtoError: if expect_errors: raise raise except: @@ -258,7 +258,7 @@ def mitm_verify(self, sig, expected_xpub): # If Pycoin is not available, do it using ecdsa from ecdsa import BadSignatureError, SECP256k1, VerifyingKey - pubkey, chaincode = decode_xpub(expected_xpub) + pubkey, _ = decode_xpub(expected_xpub) vk = VerifyingKey.from_string(get_pubkey_string(pubkey), curve=SECP256k1) try: ok = vk.verify_digest(sig[1:], self.session_key) diff --git a/hwilib/devices/ckcc/utils.py b/hwilib/devices/ckcc/utils.py index 1de4563da..630ea1a64 100644 --- a/hwilib/devices/ckcc/utils.py +++ b/hwilib/devices/ckcc/utils.py @@ -24,14 +24,14 @@ def consume(xfd, tname, fmt, names): assert dfu_prefix.signature == b'DfuSe', "Not a DFU file (bad magic)" - for idx in range(dfu_prefix.targets): + for _ in range(dfu_prefix.targets): prefix = consume(fd, 'Target', '<6sBI255s2I', 'signature altsetting named name size elements') #print("target%d: %r" % (idx, prefix)) - for ei in range(prefix.elements): + for _ in range(prefix.elements): # Decode target prefix # < little endian # I uint32_t element address diff --git a/hwilib/devices/trezorlib/protobuf.py b/hwilib/devices/trezorlib/protobuf.py index c082e41af..3001bb1f9 100644 --- a/hwilib/devices/trezorlib/protobuf.py +++ b/hwilib/devices/trezorlib/protobuf.py @@ -158,7 +158,7 @@ def __getitem__(self, key): def _fill_missing(self): # fill missing fields - for fname, ftype, fflags in self.get_fields().values(): + for fname, _, fflags in self.get_fields().values(): if not hasattr(self, fname): if fflags & FLAG_REPEATED: setattr(self, fname, []) From 3016d4a8616b042bae82bca64c2253ea2d128a19 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Tue, 11 Aug 2020 10:03:36 -0400 Subject: [PATCH 153/634] Add disclaimer to README about (lack of) endorsement --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 54eb4129e..1fe71d1cf 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ The Bitcoin Hardware Wallet Interface is a Python library and command line tool It provides a standard way for software to work with hardware wallets without needing to implement device specific drivers. Python software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool. +Caveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security. + ## Prerequisites Python 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed From 78a8801f949cb8cb12b351f81942daaf86a0a737 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sat, 15 Aug 2020 21:21:45 +0200 Subject: [PATCH 154/634] Support multisig xpub descriptors --- hwilib/commands.py | 9 ++++++--- hwilib/descriptor.py | 7 +++++++ hwilib/devices/coldcard.py | 2 +- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/ledger.py | 2 +- hwilib/devices/trezor.py | 13 +++++++++++-- hwilib/hwwclient.py | 2 ++ test/test_descriptor.py | 12 ++++++++++++ test/test_device.py | 16 +++++++++++++++- 9 files changed, 56 insertions(+), 9 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 28cb69f21..51d0278f3 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -236,15 +236,18 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede if descriptor.sh or descriptor.sh_wsh or descriptor.wsh: path = '' redeem_script = format(80 + int(descriptor.multisig_M), 'x') + xpubs_descriptor = False for i in range(0, descriptor.multisig_N): - path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] + ',' + path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] if not descriptor.path_suffix[i]: redeem_script += '21' + descriptor.base_key[i] else: - return {'error': 'Multisig descriptor must include all pubkeys', 'code': BAD_ARGUMENT} + path += descriptor.path_suffix[i] + xpubs_descriptor = True + path += ',' path = path[0:-1] redeem_script += format(80 + descriptor.multisig_N, 'x') + 'ae' - return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script) + return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None) if descriptor.m_path is None: return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} if descriptor.origin_fingerprint != client.get_master_fingerprint_hex(): diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 85f4fddd2..f95c75e39 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors import re # From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp @@ -82,6 +83,12 @@ def __init__( if origin_path and not isinstance(origin_path, list): self.m_path_base = "m" + origin_path self.m_path = "m" + origin_path + (path_suffix or "") + elif isinstance(origin_path, list): + self.m_path_base = [] + self.m_path = [] + for i in range(0, len(origin_path)): + self.m_path_base.append("m" + origin_path[i]) + self.m_path.append("m" + origin_path[i] + (path_suffix[i] or "")) @classmethod def parse(cls, desc, testnet=False): diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 7848d1bab..5011ad93e 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -205,7 +205,7 @@ def sign_message(self, message, keypath): # Display address of specified type on the device. @coldcard_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 5ff6d7fce..a857c96ed 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -549,7 +549,7 @@ def sign_message(self, message, keypath): return {"signature": base64.b64encode(compact_sig).decode('utf-8')} # Display address of specified type on the device. - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') # Setup a new device diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 0295de09f..714d225a7 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -340,7 +340,7 @@ def sign_message(self, message, keypath): # Display address of specified type on the device. Only supports single-key based addresses. @ledger_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") if redeem_script is not None: diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index bb1b69af1..72a238362 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -410,11 +410,20 @@ def sign_message(self, message, keypath): # Display address of specified type on the device. @trezor_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None): + def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): self._check_unlocked() + # descriptor means multisig with xpubs + if descriptor: + pubkeys = [] + xpub = ExtendedKey() + for i in range(0, descriptor.multisig_N): + xpub.deserialize(descriptor.base_key[i]) + hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) + pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=tools.parse_path('m' + descriptor.path_suffix[i]))) + multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys) # redeem_script means p2sh/multisig - if redeem_script: + elif redeem_script: # Get multisig object required by Trezor's get_address multisig = parse_multisig(bytes.fromhex(redeem_script)) if not multisig[0]: diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index bafc84c46..bbc213d6e 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -1,6 +1,7 @@ from typing import Dict, Optional, Union from .base58 import get_xpub_fingerprint_hex +from .descriptor import Descriptor from .serializations import PSBT @@ -73,6 +74,7 @@ def display_address( p2sh_p2wpkh: bool, bech32: bool, redeem_script: Optional[str] = None, + descriptor: Optional[Descriptor] = None, ) -> Dict[str, str]: """Display and return the address of specified type. diff --git a/test/test_descriptor.py b/test/test_descriptor.py index a479c5962..0a1252934 100755 --- a/test/test_descriptor.py +++ b/test/test_descriptor.py @@ -16,6 +16,18 @@ def test_parse_descriptor_with_origin(self): self.assertEqual(desc.testnet, True) self.assertEqual(desc.m_path, "m/84'/1'/0'/0/0") + def test_parse_multisig_descriptor_with_origin(self): + desc = Descriptor.parse("wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))", True) + self.assertIsNotNone(desc) + self.assertEqual(desc.wsh, True) + self.assertEqual(desc.origin_fingerprint, ["00000001", "00000002"]) + self.assertEqual(desc.origin_path, ["/48'/0'/0'/2'", "/48'/0'/0'/2'"]) + self.assertEqual(desc.base_key, ["tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B", "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"]) + self.assertEqual(desc.path_suffix, ["/0/0", "/0/0"]) + self.assertEqual(desc.testnet, True) + self.assertEqual(desc.m_path_base, ["m/48'/0'/0'/2'", "m/48'/0'/0'/2'"]) + self.assertEqual(desc.m_path, ["m/48'/0'/0'/2'/0/0", "m/48'/0'/0'/2'/0/0"]) + def test_parse_descriptor_without_origin(self): desc = Descriptor.parse("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) self.assertIsNotNone(desc) diff --git a/test/test_device.py b/test/test_device.py index ad245237e..7fb0bc2ce 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -13,6 +13,7 @@ from authproxy import AuthServiceProxy, JSONRPCException from hwilib.base58 import xpub_to_pub_hex from hwilib.cli import process_commands +from hwilib.descriptor import AddChecksum from hwilib.serializations import PSBT # Class for emulator control @@ -657,11 +658,24 @@ def test_display_address_multisig_descriptor(self): sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display-desc").popitem()[0] wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display-desc").popitem()[0] - # need to replace `'` with `h` and to remove checksome for the stdin option to work + # need to replace `'` with `h` and to remove checksum for the stdin option to work sh_multi_desc = sh_multi_desc.replace("'", "h").split('#')[0] sh_wsh_multi_desc = sh_wsh_multi_desc.replace("'", "h").split('#')[0] wsh_multi_desc = wsh_multi_desc.replace("'", "h").split('#')[0] + # descriptor with xpubs + if self.full_type == 'trezor_t': + account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/45h/0h/0h/2h'])['xpub'] + desc = 'wsh(multi(2,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/0/0,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/1/0))' + result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + addr = self.wrpc.deriveaddresses(AddChecksum(desc))[0] + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + # legacy result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_multi_desc]) self.assertNotIn('error', result) From 287bce98942483de0889cb152d714a39713e5e44 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Thu, 13 Aug 2020 00:25:28 +0200 Subject: [PATCH 155/634] added py.typed so that static type checkers use package stubs (PEP 561) --- hwilib/py.typed | 0 pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 hwilib/py.typed diff --git a/hwilib/py.typed b/hwilib/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/pyproject.toml b/pyproject.toml index 533a88c8b..4f976fdf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ readme = "README.md" repository = "https://github.com/bitcoin-core/HWI" homepage = "https://github.com/bitcoin-core/HWI" exclude = ["docs/", "test/"] -include = ["hwilib/**/*.py", "udev/"] +include = ["hwilib/**/*.py", "udev/", "hwilib/py.typed"] packages = [ { include = "hwi.py" }, { include = "hwi-qt.py" }, From 7a896e792be4bf33a1511dfc8ba03352f08c5bcb Mon Sep 17 00:00:00 2001 From: "Ferdinando M. Ametrano" Date: Mon, 17 Aug 2020 15:03:22 +0200 Subject: [PATCH 156/634] added gitignore directives for virtual environments virtual environments are routinely created in the root project (sub)folder(s) and should be ignored by Git --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 5d534bb44..843b1be04 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,12 @@ hwilib/ui/ui_*.py *.stderr *.stdout + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ From 5e296ca4dca8fbf3f126ecbff507ff926808f13a Mon Sep 17 00:00:00 2001 From: "Ferdinando M. Ametrano" Date: Mon, 17 Aug 2020 12:02:38 +0200 Subject: [PATCH 157/634] Cleaned up log messages --- hwilib/devices/trezorlib/client.py | 2 +- hwilib/devices/trezorlib/device.py | 5 ++++- hwilib/devices/trezorlib/transport/__init__.py | 4 ++-- hwilib/devices/trezorlib/transport/hid.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index 2abb87264..94284f557 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -72,7 +72,7 @@ class TrezorClient: """ def __init__(self, transport, ui=None, state=None): - LOG.info("creating client instance for device: {}".format(transport.get_path())) + LOG.debug("creating client instance for device: {}".format(transport.get_path())) self.transport = transport self.ui = ui self.state = state diff --git a/hwilib/devices/trezorlib/device.py b/hwilib/devices/trezorlib/device.py index 329e79df7..9605bd128 100644 --- a/hwilib/devices/trezorlib/device.py +++ b/hwilib/devices/trezorlib/device.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +import logging import os import time import warnings @@ -25,6 +26,8 @@ RECOVERY_BACK = "\x08" # backspace character, sent literally +LOG = logging.getLogger(__name__) + class TrezorDevice: """ @@ -192,7 +195,7 @@ def reset( raise RuntimeError("Invalid response, expected EntropyRequest") external_entropy = os.urandom(32) - # LOG.debug("Computer generated entropy: " + external_entropy.hex()) + LOG.debug("Computer generated entropy: " + external_entropy.hex()) ret = client.call(proto.EntropyAck(entropy=external_entropy)) client.init_device() return ret diff --git a/hwilib/devices/trezorlib/transport/__init__.py b/hwilib/devices/trezorlib/transport/__init__.py index 8a7685b38..33ff23ba4 100644 --- a/hwilib/devices/trezorlib/transport/__init__.py +++ b/hwilib/devices/trezorlib/transport/__init__.py @@ -121,7 +121,7 @@ def enumerate_devices() -> Iterable[Transport]: name = transport.__name__ try: found = list(transport.enumerate()) - LOG.info("Enumerating {}: found {} devices".format(name, len(found))) + LOG.debug("Enumerating {}: found {} devices".format(name, len(found))) devices.extend(found) except NotImplementedError: LOG.error("{} does not implement device enumeration".format(name)) @@ -144,7 +144,7 @@ def get_transport(path: str = None, prefix_search: bool = False) -> Transport: def match_prefix(a: str, b: str) -> bool: return a.startswith(b) or b.startswith(a) - LOG.info( + LOG.debug( "looking for device by {}: {}".format( "prefix" if prefix_search else "full path", path ) diff --git a/hwilib/devices/trezorlib/transport/hid.py b/hwilib/devices/trezorlib/transport/hid.py index 8ab443818..e9b816720 100644 --- a/hwilib/devices/trezorlib/transport/hid.py +++ b/hwilib/devices/trezorlib/transport/hid.py @@ -27,7 +27,7 @@ try: import hid except Exception as e: - LOG.info("HID transport is disabled: {}".format(e)) + LOG.warning("HID transport is disabled: {}".format(e)) hid = None From 79f74a78fcd0a8d182e8f80e5b5f00b338d16155 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 13:22:49 -0400 Subject: [PATCH 158/634] tests: refactor --testnet in self.dev_args to __init__ --- test/test_device.py | 10 +--------- test/test_digitalbitbox.py | 3 +++ test/test_ledger.py | 3 +++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 7fb0bc2ce..48ef0d564 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -65,7 +65,7 @@ def __init__(self, rpc, rpc_userpass, type, full_type, path, fingerprint, master self.fingerprint = fingerprint self.master_xpub = master_xpub self.password = password - self.dev_args = ['-t', self.type, '-d', self.path] + self.dev_args = ['-t', self.type, '-d', self.path, '--testnet'] if emulator: self.emulator = emulator else: @@ -172,8 +172,6 @@ def setUp(self): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - if '--testnet' not in self.dev_args: - self.dev_args.append('--testnet') self.emulator.start() def test_getkeypool(self): @@ -249,8 +247,6 @@ def test_getkeypool(self): class TestGetDescriptors(DeviceTestCase): def setUp(self): self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) - if '--testnet' not in self.dev_args: - self.dev_args.append('--testnet') self.emulator.start() def tearDown(self): @@ -281,8 +277,6 @@ def setUp(self): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - if '--testnet' not in self.dev_args: - self.dev_args.append('--testnet') self.emulator.start() def _generate_and_finalize(self, unknown_inputs, psbt): @@ -462,8 +456,6 @@ def setUp(self): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - if '--testnet' not in self.dev_args: - self.dev_args.append('--testnet') self.emulator.start() def test_display_address_bad_args(self): diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 9d196ee9f..95198881b 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -133,6 +133,9 @@ def test_backup(self): self.assertTrue(result['success']) class TestBitboxGetXpub(DeviceTestCase): + def setUp(self): + self.dev_args.remove('--testnet') + def test_getxpub(self): result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) self.assertEqual(result['xpub'], 'xpub6Du9e5Cz1NZWz3dvsvM21tsj4xEdbAb7AcbysFL42Y3yr8PLMnsaxhetHxurTpX5Rp5RbnFFwP1wct8K3gErCUSwcxFhxThsMBSxdmkhTNf') diff --git a/test/test_ledger.py b/test/test_ledger.py index 0c5f3612e..d4281a5ff 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -99,6 +99,9 @@ def test_backup(self): self.assertEqual(result['code'], -9) class TestLedgerGetXpub(DeviceTestCase): + def setUp(self): + self.dev_args.remove("--testnet") + def test_getxpub(self): result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) self.assertEqual(result['xpub'], 'xpub6DqTtMuqBiBsSPb5UxB1qgJ3ViXuhoyZYhw3zTK4MywLB6psioW4PN1SAbhxVVirKQojnTBsjG5gXiiueRBgWmUuN43dpbMSgMCQHVqx2bR') From 4498c3f347f129213c3c1c0f035dddb04764f079 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 13:31:48 -0400 Subject: [PATCH 159/634] tests: Use parent class setUp and don't re-set self.rpc self.rpc is already setup when the test starts, no need to set it again. Additionally, use the parent class setUp so we aren't duplicating as much code. --- test/test_device.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 48ef0d564..3115c2fee 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -167,12 +167,11 @@ def test_type_only_autodetect(self): class TestGetKeypool(DeviceTestCase): def setUp(self): - self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) + super().setUp() if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - self.emulator.start() def test_getkeypool(self): non_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--nokeypool', '0', '20']) @@ -245,10 +244,6 @@ def test_getkeypool(self): self.assertEqual(keypool_desc['code'], -7) class TestGetDescriptors(DeviceTestCase): - def setUp(self): - self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) - self.emulator.start() - def tearDown(self): self.emulator.stop() @@ -272,12 +267,11 @@ def test_getdescriptors(self): class TestSignTx(DeviceTestCase): def setUp(self): - self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) + super().setUp() if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - self.emulator.start() def _generate_and_finalize(self, unknown_inputs, psbt): if not unknown_inputs: @@ -451,12 +445,11 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def setUp(self): - self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass)) + super().setUp() if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): self.rpc.createwallet('{}_test'.format(self.full_type), True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) - self.emulator.start() def test_display_address_bad_args(self): result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) From 66182f0ca7a74ee0e268b06a0915b608fb353ad4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 14:00:40 -0400 Subject: [PATCH 160/634] tests: Refactor wallet creation to parent class function --- test/test_device.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 3115c2fee..4b4cf0734 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -114,6 +114,12 @@ def __str__(self): def __repr__(self): return '{}: {}'.format(self.full_type, super().__repr__()) + def setup_wallets(self): + if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): + self.rpc.createwallet('{}_test'.format(self.full_type), True) + self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) + self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) + def setUp(self): self.emulator.start() @@ -168,10 +174,7 @@ def test_type_only_autodetect(self): class TestGetKeypool(DeviceTestCase): def setUp(self): super().setUp() - if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): - self.rpc.createwallet('{}_test'.format(self.full_type), True) - self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) - self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) + self.setup_wallets() def test_getkeypool(self): non_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--nokeypool', '0', '20']) @@ -268,10 +271,7 @@ def test_getdescriptors(self): class TestSignTx(DeviceTestCase): def setUp(self): super().setUp() - if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): - self.rpc.createwallet('{}_test'.format(self.full_type), True) - self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) - self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) + self.setup_wallets() def _generate_and_finalize(self, unknown_inputs, psbt): if not unknown_inputs: @@ -446,10 +446,7 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def setUp(self): super().setUp() - if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): - self.rpc.createwallet('{}_test'.format(self.full_type), True) - self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) - self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) + self.setup_wallets() def test_display_address_bad_args(self): result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) From b9931556b17ede3b6cae582c7fa4acc633a417a6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 15:42:57 -0400 Subject: [PATCH 161/634] tests: Refactor multisig setup in multisig displayaddress The path and descriptor tests do basically the same thing to setup the multisig. Refactor that out to be separate and to not rely on the wallet. --- test/test_device.py | 189 +++++++++++++------------------------------- 1 file changed, 56 insertions(+), 133 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 4b4cf0734..6bf332214 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -521,129 +521,61 @@ def test_display_address_descriptor(self): self.assertIn('code', result) self.assertEqual(result['code'], -7) - def test_display_address_multisig_path(self): - supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} - if self.full_type not in supports_multisig: - return - # Import some keys to the watch only wallet and get multisig address - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '40', '50']) - import_result = self.wrpc.importmulti(keypool_desc) - self.assertTrue(import_result[0]['success']) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '40', '50']) - import_result = self.wrpc.importmulti(keypool_desc) - self.assertTrue(import_result[0]['success']) - sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') - wpkh_addr = self.wrpc.getnewaddress('', 'bech32') - pkh_addr = self.wrpc.getnewaddress('', 'legacy') - self.wrpc.importaddress(wpkh_addr) - self.wrpc.importaddress(pkh_addr) - - # pubkeys to construct 2-of-3 multisig descriptors for import - sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) - wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) - pkh_info = self.wrpc.getaddressinfo(pkh_addr) - - pubkeys = [sh_wpkh_info['desc'][8:-11], - wpkh_info['desc'][5:-10], - pkh_info['desc'][4:-10]] - - # Get the descriptors with their checksums - sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] - sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] - wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] - - sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display"} - sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display"} - # re-order pubkeys to allow import without "already have private keys" error - wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display"} - multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) - self.assertTrue(multi_result[0]['success']) - self.assertTrue(multi_result[1]['success']) - self.assertTrue(multi_result[2]['success']) + def _make_single_multisig(self, addrtype): + desc_pubkeys = [] + sorted_pubkeys = [] + for i in range(0, 3): + path = "/49h/1h/0h/0/{}".format(i) + origin = '{}{}'.format(self.fingerprint, path) + xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) + desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) + sorted_pubkeys.append((xpub["pubkey"], origin)) + sorted_pubkeys.sort(key=lambda tup: tup[0]) + + if addrtype == "pkh": + desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2])) + ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "legacy") + elif addrtype == "sh_wpkh": + desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format(desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0])) + ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "p2sh-segwit") + elif addrtype == "wpkh": + desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0])) + ms_info = self.rpc.createmultisig(2, [x[0] for x in sorted_pubkeys], "bech32") + else: + self.fail("Oops the test is broken") - sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display").popitem()[0] - sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display").popitem()[0] - wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display").popitem()[0] + self.assertEqual(self.rpc.deriveaddresses(desc)[0], ms_info["address"]) - sh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] - sh_wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] - wsh_multi_addr_redeem_script = self.wrpc.getaddressinfo(sh_multi_addr)['hex'] + path = "{},{},{}".format(sorted_pubkeys[0][1], sorted_pubkeys[1][1], sorted_pubkeys[2][1]) - path = pubkeys[2][1:24] + ',' + pubkeys[1][1:24] + ',' + pubkeys[0][1:24] - # need to replace `'` with `h` for stdin option to work - path = path.replace("'", "h") + return ms_info["address"], desc, ms_info["redeemScript"], path - # legacy - result = self.do_command(self.dev_args + ['displayaddress', '--path', path, '--redeem_script', sh_multi_addr_redeem_script]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - self.assertEqual(sh_multi_addr, result['address']) + def test_display_address_multisig_path(self): + supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} + if self.full_type not in supports_multisig: + return - # wrapped segwit - result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', path, '--redeem_script', sh_wsh_multi_addr_redeem_script]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - self.assertEqual(sh_wsh_multi_addr, result['address']) + for addrtype in ["pkh", "sh_wpkh", "wpkh"]: + addr, desc, rs, path = self._make_single_multisig(addrtype) + args = ['displayaddress', '--path', path, '--redeem_script', rs] + if addrtype != "pkh": + args.append("--{}".format(addrtype)) + result = self.do_command(self.dev_args + args) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) - # native setwit - result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', path, '--redeem_script', wsh_multi_addr_redeem_script]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56]) + if addrtype == "wpkh": + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + else: + self.assertEqual(addr, result['address']) def test_display_address_multisig_descriptor(self): supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} if self.full_type not in supports_multisig: return - # Import some keys to the watch only wallet and get multisig address - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '50', '60']) - import_result = self.wrpc.importmulti(keypool_desc) - self.assertTrue(import_result[0]['success']) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '50', '60']) - import_result = self.wrpc.importmulti(keypool_desc) - self.assertTrue(import_result[0]['success']) - sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') - wpkh_addr = self.wrpc.getnewaddress('', 'bech32') - pkh_addr = self.wrpc.getnewaddress('', 'legacy') - self.wrpc.importaddress(wpkh_addr) - self.wrpc.importaddress(pkh_addr) - - # pubkeys to construct 2-of-3 multisig descriptors for import - sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) - wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) - pkh_info = self.wrpc.getaddressinfo(pkh_addr) - - pubkeys = [sh_wpkh_info['desc'][8:-11], - wpkh_info['desc'][5:-10], - pkh_info['desc'][4:-10]] - - # Get the descriptors with their checksums - sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] - sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] - wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] - - sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti-display-desc"} - sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti-display-desc"} - # re-order pubkeys to allow import without "already have private keys" error - wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti-display-desc"} - multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) - self.assertTrue(multi_result[0]['success']) - self.assertTrue(multi_result[1]['success']) - self.assertTrue(multi_result[2]['success']) - - sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti-display-desc").popitem()[0] - sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display-desc").popitem()[0] - wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display-desc").popitem()[0] - - # need to replace `'` with `h` and to remove checksum for the stdin option to work - sh_multi_desc = sh_multi_desc.replace("'", "h").split('#')[0] - sh_wsh_multi_desc = sh_wsh_multi_desc.replace("'", "h").split('#')[0] - wsh_multi_desc = wsh_multi_desc.replace("'", "h").split('#')[0] # descriptor with xpubs if self.full_type == 'trezor_t': @@ -658,28 +590,19 @@ def test_display_address_multisig_descriptor(self): # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix self.assertEqual(addr[4:58], result['address'][2:56]) - # legacy - result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_multi_desc]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - self.assertEqual(sh_multi_addr, result['address']) - - # wrapped segwit - result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_wsh_multi_desc]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - self.assertEqual(sh_wsh_multi_addr, result['address']) + for addrtype in ["pkh", "sh_wpkh", "wpkh"]: + addr, desc, rs, path = self._make_single_multisig(addrtype) + result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) - # native setwit - result = self.do_command(self.dev_args + ['displayaddress', '--desc', wsh_multi_desc]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(wsh_multi_addr[4:58], result['address'][2:56]) + if addrtype == "wpkh": + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + else: + self.assertEqual(addr, result['address']) class TestSignMessage(DeviceTestCase): def test_sign_msg(self): From e8316657a57aa6d5d4dade5a13493d46041402b7 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 15:43:44 -0400 Subject: [PATCH 162/634] test: Remove wallet setup from displayaddress tests This is no longer needed. --- test/test_device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 6bf332214..61fec8dec 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -444,10 +444,6 @@ def test_big_tx(self): pass class TestDisplayAddress(DeviceTestCase): - def setUp(self): - super().setUp() - self.setup_wallets() - def test_display_address_bad_args(self): result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) self.assertIn('error', result) From c6038cbbd9a018f03b0fcde930f3e7790cb05bdd Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 15:44:09 -0400 Subject: [PATCH 163/634] tests: Use a unique wallet name for each test case Instead of a single wallet for all tests, make a wallet for each individual test case (i.e. test function). --- test/test_device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 61fec8dec..2e39f23f7 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -115,9 +115,9 @@ def __repr__(self): return '{}: {}'.format(self.full_type, super().__repr__()) def setup_wallets(self): - if '{}_test'.format(self.full_type) not in self.rpc.listwallets(): - self.rpc.createwallet('{}_test'.format(self.full_type), True) - self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type)) + wallet_name = '{}_{}_test'.format(self.full_type, self.id()) + self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=True) + self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}'.format(self.rpc_userpass, wallet_name)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) def setUp(self): From 84eacd63e1184486dd72312c695bbb3bd2ddc474 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 15:59:24 -0400 Subject: [PATCH 164/634] tests: Properly skip multisig displayaddress for unsupported devices --- test/test_device.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 2e39f23f7..0d90102ab 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -16,6 +16,8 @@ from hwilib.descriptor import AddChecksum from hwilib.serializations import PSBT +SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} + # Class for emulator control class DeviceEmulator(): def start(self): @@ -547,9 +549,8 @@ def _make_single_multisig(self, addrtype): return ms_info["address"], desc, ms_info["redeemScript"], path def test_display_address_multisig_path(self): - supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} - if self.full_type not in supports_multisig: - return + if self.full_type not in SUPPORTS_MS_DISPLAY: + raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) for addrtype in ["pkh", "sh_wpkh", "wpkh"]: addr, desc, rs, path = self._make_single_multisig(addrtype) @@ -569,9 +570,8 @@ def test_display_address_multisig_path(self): self.assertEqual(addr, result['address']) def test_display_address_multisig_descriptor(self): - supports_multisig = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} - if self.full_type not in supports_multisig: - return + if self.full_type not in SUPPORTS_MS_DISPLAY: + raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) # descriptor with xpubs if self.full_type == 'trezor_t': From 10f094a0b0fe02280b1450c56febed0fd4778a45 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 16:06:35 -0400 Subject: [PATCH 165/634] tests: move xpub multisig displayaddress to separate test case --- test/test_device.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 0d90102ab..2fca97da1 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -17,6 +17,7 @@ from hwilib.serializations import PSBT SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} +SUPPORTS_XPUB_MS_DISPLAY = {'trezor_t'} # Class for emulator control class DeviceEmulator(): @@ -573,19 +574,6 @@ def test_display_address_multisig_descriptor(self): if self.full_type not in SUPPORTS_MS_DISPLAY: raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) - # descriptor with xpubs - if self.full_type == 'trezor_t': - account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/45h/0h/0h/2h'])['xpub'] - desc = 'wsh(multi(2,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/0/0,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/1/0))' - result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - addr = self.wrpc.deriveaddresses(AddChecksum(desc))[0] - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(addr[4:58], result['address'][2:56]) - for addrtype in ["pkh", "sh_wpkh", "wpkh"]: addr, desc, rs, path = self._make_single_multisig(addrtype) result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) @@ -600,6 +588,21 @@ def test_display_address_multisig_descriptor(self): else: self.assertEqual(addr, result['address']) + def test_display_address_xpub_multisig(self): + if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: + raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type)) + + account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/45h/0h/0h/2h'])['xpub'] + desc = 'wsh(multi(2,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/0/0,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/1/0))' + result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + addr = self.rpc.deriveaddresses(AddChecksum(desc))[0] + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + class TestSignMessage(DeviceTestCase): def test_sign_msg(self): self.do_command(self.dev_args + ['signmessage', '"Message signing test"', 'm/44h/1h/0h/0/0']) From f0cef6ea39baacb060de982ffd65e10a4d31a68c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 16:09:24 -0400 Subject: [PATCH 166/634] tests: Combine multisig displayaddress path and descriptor tests Combine these because they're basically the same. Also uses subtests. --- test/test_device.py | 57 ++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 2fca97da1..ef06a5540 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -549,44 +549,33 @@ def _make_single_multisig(self, addrtype): return ms_info["address"], desc, ms_info["redeemScript"], path - def test_display_address_multisig_path(self): + def test_display_address_multisig(self): if self.full_type not in SUPPORTS_MS_DISPLAY: raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) for addrtype in ["pkh", "sh_wpkh", "wpkh"]: - addr, desc, rs, path = self._make_single_multisig(addrtype) - args = ['displayaddress', '--path', path, '--redeem_script', rs] - if addrtype != "pkh": - args.append("--{}".format(addrtype)) - result = self.do_command(self.dev_args + args) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - - if addrtype == "wpkh": - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(addr[4:58], result['address'][2:56]) - else: - self.assertEqual(addr, result['address']) - - def test_display_address_multisig_descriptor(self): - if self.full_type not in SUPPORTS_MS_DISPLAY: - raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) - - for addrtype in ["pkh", "sh_wpkh", "wpkh"]: - addr, desc, rs, path = self._make_single_multisig(addrtype) - result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - - if addrtype == "wpkh": - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(addr[4:58], result['address'][2:56]) - else: - self.assertEqual(addr, result['address']) + for use_desc in [True, False]: + with self.subTest(addrtype=addrtype, use_desc=use_desc): + addr, desc, rs, path = self._make_single_multisig(addrtype) + + if use_desc: + args = ['displayaddress', '--desc', desc] + else: + args = ['displayaddress', '--path', path, '--redeem_script', rs] + if addrtype != "pkh": + args.append("--{}".format(addrtype)) + + result = self.do_command(self.dev_args + args) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + + if addrtype == "wpkh": + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + else: + self.assertEqual(addr, result['address']) def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: From c5a64885bc70d7834a163035f133e29ae92ec89e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 17:26:01 -0400 Subject: [PATCH 167/634] tests: Refactor multisig signtx to be able to use a separate path --- test/test_device.py | 49 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index ef06a5540..033a337e2 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -325,49 +325,52 @@ def _generate_and_finalize(self, unknown_inputs, psbt): self.assertTrue(self.wrpc.testmempoolaccept([finalize_res['hex']])[0]["allowed"]) return finalize_res['hex'] + def _make_multisigs(self): + desc_pubkeys = [] + sorted_pubkeys = [] + for i in range(0, 3): + path = "/49h/1h/0h/0/{}".format(i) + origin = '{}{}'.format(self.fingerprint, path) + xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) + desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) + sorted_pubkeys.append(xpub["pubkey"]) + sorted_pubkeys.sort() + + sh_desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2])) + sh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "legacy") + self.assertEqual(self.rpc.deriveaddresses(sh_desc)[0], sh_ms_info["address"]) + + sh_wsh_desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format(desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0])) + sh_wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "p2sh-segwit") + self.assertEqual(self.rpc.deriveaddresses(sh_wsh_desc)[0], sh_wsh_ms_info["address"]) + + wsh_desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0])) + wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "bech32") + self.assertEqual(self.rpc.deriveaddresses(wsh_desc)[0], wsh_ms_info["address"]) + + return sh_desc, sh_ms_info["address"], sh_wsh_desc, sh_wsh_ms_info["address"], wsh_desc, wsh_ms_info["address"] + def _test_signtx(self, input_type, multisig, external): # Import some keys to the watch only wallet and send coins to them keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '50']) import_result = self.wrpc.importmulti(keypool_desc) self.assertTrue(import_result[0]['success']) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--internal', '30', '50']) - import_result = self.wrpc.importmulti(keypool_desc) - self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') wpkh_addr = self.wrpc.getnewaddress('', 'bech32') pkh_addr = self.wrpc.getnewaddress('', 'legacy') self.wrpc.importaddress(wpkh_addr) self.wrpc.importaddress(pkh_addr) - # pubkeys to construct 2-of-3 multisig descriptors for import - sh_wpkh_info = self.wrpc.getaddressinfo(sh_wpkh_addr) - wpkh_info = self.wrpc.getaddressinfo(wpkh_addr) - pkh_info = self.wrpc.getaddressinfo(pkh_addr) - - # Get origin info/key pair so wallet doesn't forget how to - # sign with keys post-import - pubkeys = [sh_wpkh_info['desc'][8:-11], - wpkh_info['desc'][5:-10], - pkh_info['desc'][4:-10]] - - # Get the descriptors with their checksums - sh_multi_desc = self.wrpc.getdescriptorinfo('sh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + '))')['descriptor'] - sh_wsh_multi_desc = self.wrpc.getdescriptorinfo('sh(wsh(sortedmulti(2,' + pubkeys[0] + ',' + pubkeys[1] + ',' + pubkeys[2] + ')))')['descriptor'] - wsh_multi_desc = self.wrpc.getdescriptorinfo('wsh(sortedmulti(2,' + pubkeys[2] + ',' + pubkeys[1] + ',' + pubkeys[0] + '))')['descriptor'] + sh_multi_desc, sh_multi_addr, sh_wsh_multi_desc, sh_wsh_multi_addr, wsh_multi_desc, wsh_multi_addr = self._make_multisigs() sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti"} sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti"} - # re-order pubkeys to allow import without "already have private keys" error wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti"} multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) self.assertTrue(multi_result[0]['success']) self.assertTrue(multi_result[1]['success']) self.assertTrue(multi_result[2]['success']) - sh_multi_addr = self.wrpc.getaddressesbylabel("shmulti").popitem()[0] - sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti").popitem()[0] - wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti").popitem()[0] - in_amt = 3 out_amt = in_amt // 3 number_inputs = 0 From 991a14473da3d17e47aca0ef575b8ceffafe237b Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Thu, 20 Aug 2020 12:55:23 +0200 Subject: [PATCH 168/634] Update docs wrt Trezor T mixed input support --- README.md | 2 +- docs/trezor.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fe71d1cf..889036c23 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Please also see [docs](docs/) for additional information about each device. | Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | | Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | | Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes | +| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | | Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | ## Using with Bitcoin Core diff --git a/docs/trezor.md b/docs/trezor.md index 64a8f2985..85e1b7e8f 100644 --- a/docs/trezor.md +++ b/docs/trezor.md @@ -21,7 +21,6 @@ Due to the limitations of the Trezor, some transactions cannot be signed by a Tr - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. * Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. * Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. -- For **Trezor T**, a transaction cannot contain both segwit and non-segwit inputs ## Note on `backup` From 4791841d63282caf26db542ca9bea231d4d1f550 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 17:56:04 -0400 Subject: [PATCH 169/634] tests: Use descriptor wallets We are really targeting descriptor wallets, so we should be testing those. Because descriptor wallets also change how the imports will work w.r.t rescans and the next address to give out, the getkeypool path checks are changed to just check that the begin with the correct path prefix. --- test/test_device.py | 77 ++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 033a337e2..2ac2397e9 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -119,7 +119,7 @@ def __repr__(self): def setup_wallets(self): wallet_name = '{}_{}_test'.format(self.full_type, self.id()) - self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=True) + self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=True, descriptors=True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}'.format(self.rpc_userpass, wallet_name)) self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) @@ -180,67 +180,60 @@ def setUp(self): self.setup_wallets() def test_getkeypool(self): - non_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--nokeypool', '0', '20']) - import_result = self.wpk_rpc.importmulti(non_keypool_desc) - self.assertTrue(import_result[0]['success']) - pkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '0', '20']) - import_result = self.wpk_rpc.importmulti(pkh_keypool_desc) - self.assertFalse(import_result[0]['success']) - - import_result = self.wrpc.importmulti(pkh_keypool_desc) + import_result = self.wrpc.importdescriptors(pkh_keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/44'/1'/0'/0/{}".format(i)) - addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/44'/1'/0'/1/{}".format(i)) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'legacy')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/44'/1'/0'/0/")) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('legacy')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/44'/1'/0'/1/")) shwpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '0', '20']) - import_result = self.wrpc.importmulti(shwpkh_keypool_desc) + import_result = self.wrpc.importdescriptors(shwpkh_keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): + for _ in range(0, 21): addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit')) - self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/0/{}".format(i)) + self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/0'/0/")) addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) - self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/0'/1/{}".format(i)) + self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/0'/1/")) wpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20']) - import_result = self.wrpc.importmulti(wpkh_keypool_desc) + import_result = self.wrpc.importdescriptors(wpkh_keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/0'/0/{}".format(i)) - addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/0'/1/{}".format(i)) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/0'/0/")) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/0'/1/")) # Test that `--all` option gives the "concatenation" of previous three calls all_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '0', '20']) self.assertEqual(all_keypool_desc, pkh_keypool_desc + wpkh_keypool_desc + shwpkh_keypool_desc) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--account', '3', '0', '20']) - import_result = self.wrpc.importmulti(keypool_desc) + import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): + for _ in range(0, 21): addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'p2sh-segwit')) - self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/0/{}".format(i)) + self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/0/")) addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) - self.assertEqual(addr_info['hdkeypath'], "m/49'/1'/3'/1/{}".format(i)) + self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/1/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '--account', '3', '0', '20']) - import_result = self.wrpc.importmulti(keypool_desc) + import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/3'/0/{}".format(i)) - addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/84'/1'/3'/1/{}".format(i)) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/3'/0/")) + addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/84'/1'/3'/1/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) - import_result = self.wrpc.importmulti(keypool_desc) + import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) - for i in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress()) - self.assertEqual(addr_info['hdkeypath'], "m/0'/0'/4'/{}".format(i)) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'legacy')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/0'/0'/4'/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', '/0h/0h/4h/*', '0', '20']) self.assertEqual(keypool_desc['error'], 'Path must start with m/') @@ -352,21 +345,19 @@ def _make_multisigs(self): def _test_signtx(self, input_type, multisig, external): # Import some keys to the watch only wallet and send coins to them - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '30', '50']) - import_result = self.wrpc.importmulti(keypool_desc) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '30', '50']) + import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') wpkh_addr = self.wrpc.getnewaddress('', 'bech32') pkh_addr = self.wrpc.getnewaddress('', 'legacy') - self.wrpc.importaddress(wpkh_addr) - self.wrpc.importaddress(pkh_addr) sh_multi_desc, sh_multi_addr, sh_wsh_multi_desc, sh_wsh_multi_addr, wsh_multi_desc, wsh_multi_addr = self._make_multisigs() sh_multi_import = {'desc': sh_multi_desc, "timestamp": "now", "label": "shmulti"} sh_wsh_multi_import = {'desc': sh_wsh_multi_desc, "timestamp": "now", "label": "shwshmulti"} wsh_multi_import = {'desc': wsh_multi_desc, "timestamp": "now", "label": "wshmulti"} - multi_result = self.wrpc.importmulti([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) + multi_result = self.wrpc.importdescriptors([sh_multi_import, sh_wsh_multi_import, wsh_multi_import]) self.assertTrue(multi_result[0]['success']) self.assertTrue(multi_result[1]['success']) self.assertTrue(multi_result[2]['success']) From 2ea932ed7834c45b1e2d275328514affa891fe8e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 19 Aug 2020 22:33:52 -0400 Subject: [PATCH 170/634] tests: Use m/48' for multisig tests Apparently m/48' (where 48 is the purpose number in a BIP 44 path) is the standard path for use with multisigs. This is mistakenly called BIP 48 in many places, but no such BIP 48 actually exists. Anyways, Trezor requires multisigs use m/48' for multisigs, so we need to use them in our tests. --- test/data/coldcard-multisig-setup.patch | 12 ++++++------ test/test_device.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/data/coldcard-multisig-setup.patch b/test/data/coldcard-multisig-setup.patch index 151353b59..4a45ec549 100644 --- a/test/data/coldcard-multisig-setup.patch +++ b/test/data/coldcard-multisig-setup.patch @@ -1,4 +1,4 @@ -From cadbd3d25306b43060fd06eed589947d537a5ced Mon Sep 17 00:00:00 2001 +From 8793fbaa9b32f3c67f289a05194e68acc5c61b7d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 17:56:05 -0500 Subject: [PATCH 2/2] Change default simulator multisig @@ -8,7 +8,7 @@ Subject: [PATCH 2/2] Change default simulator multisig 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py -index 0313c3e..e2c3d71 100644 +index 0313c3e..6d301d4 100644 --- a/unix/frozen-modules/sim_settings.py +++ b/unix/frozen-modules/sim_settings.py @@ -68,7 +68,11 @@ if '--ms' in sys.argv: @@ -17,13 +17,13 @@ index 0313c3e..e2c3d71 100644 # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator - sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] + sim_defaults['multisig'] = [ -+ ['mstest', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 8, 'ch': 'XTN'}], -+ ['mstest1', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 14, 'ch': 'XTN'}], -+ ['mstest2', [2, 3], [[1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj'], [1130956047, 'tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj']], {'ft': 26, 'ch': 'XTN'}], ++ ['mstest', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 8, 'ch': 'XTN'}], ++ ['mstest1', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 14, 'ch': 'XTN'}], ++ ['mstest2', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 26, 'ch': 'XTN'}], + ] sim_defaults['fee_limit'] = -1 if '--xfp' in sys.argv: -- -2.27.0 +2.28.0 diff --git a/test/test_device.py b/test/test_device.py index 2ac2397e9..bbd4c2aa7 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -322,7 +322,7 @@ def _make_multisigs(self): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): - path = "/49h/1h/0h/0/{}".format(i) + path = "/48h/1h/{}h/0/0".format(i) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) @@ -518,7 +518,7 @@ def _make_single_multisig(self, addrtype): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): - path = "/49h/1h/0h/0/{}".format(i) + path = "/48h/1h/{}h/0/0".format(i) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) @@ -575,8 +575,8 @@ def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type)) - account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/45h/0h/0h/2h'])['xpub'] - desc = 'wsh(multi(2,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/0/0,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/1/0))' + account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h'])['xpub'] + desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/1/0))' result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) self.assertNotIn('error', result) self.assertNotIn('code', result) From 2c381fa2dc4a9d337f42582faf763e27a3fbd885 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 20 Aug 2020 13:00:57 -0400 Subject: [PATCH 171/634] tests: Add trezor t back to mixed and external signing tests --- test/test_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index bbd4c2aa7..25e40fe48 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -403,9 +403,9 @@ def _test_signtx(self, input_type, multisig, external): # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): - supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'} + supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'} supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} - supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard'} + supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external) self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external) if self.full_type in supports_mixed: From 44dabcfa805797bef310471fc566f29ab67bb6d5 Mon Sep 17 00:00:00 2001 From: Ferdinando Ametrano Date: Tue, 18 Aug 2020 07:59:28 +0200 Subject: [PATCH 172/634] Avoided bare expect --- hwilib/commands.py | 2 +- hwilib/devices/btchip/btchip.py | 4 ++-- hwilib/devices/btchip/btchipComm.py | 6 +++--- hwilib/devices/ckcc/client.py | 5 +++-- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/trezor.py | 4 ++-- hwilib/devices/trezorlib/client.py | 2 +- hwilib/devices/trezorlib/transport/webusb.py | 2 +- test/test_coldcard.py | 2 +- test/test_digitalbitbox.py | 2 +- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 51d0278f3..b7ff7ec7b 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -69,7 +69,7 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False): client.close() continue return client - except: + except Exception: if client: client.close() pass # Ignore things we wouldn't get fingerprints for diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index 697fd1469..9b14bbc55 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -84,7 +84,7 @@ def __init__(self, dongle): self.scriptBlockLength = 50 else: self.scriptBlockLength = 255 - except: + except Exception: pass def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): @@ -271,7 +271,7 @@ def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): response = self.dongle.exchange(bytearray(apdu)) offset += dataLength alternateEncoding = True - except: + except Exception: pass if not alternateEncoding: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] diff --git a/hwilib/devices/btchip/btchipComm.py b/hwilib/devices/btchip/btchipComm.py index c67ada599..6e23140d9 100644 --- a/hwilib/devices/btchip/btchipComm.py +++ b/hwilib/devices/btchip/btchipComm.py @@ -142,7 +142,7 @@ def close(self): if self.opened: try: self.device.close() - except: + except Exception: pass self.opened = False @@ -155,7 +155,7 @@ def __init__(self, server, port, debug=False): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.socket.connect((self.server, self.port)) - except: + except Exception: raise BTChipException("Proxy connection failed") def exchange(self, apdu, timeout=20000): @@ -175,5 +175,5 @@ def exchange(self, apdu, timeout=20000): def close(self): try: self.socket.close() - except: + except Exception: pass diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index f170e7347..665da3a7e 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -162,7 +162,7 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=1000, encrypt=T except CCProtoError: if expect_errors: raise raise - except: + except Exception: #print("Corrupt response: %r" % resp) raise @@ -376,7 +376,8 @@ def close(self): self.pipe.close() try: os.unlink(self.pipe_name) - except: pass + except Exception: + pass def get_serial_number_string(self): return 'simulator' diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index d414ddf80..70ef6c5f7 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -631,7 +631,7 @@ def enumerate(password=''): dev.send_recv(b'{"device" : "info"}') devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0}) dev.close() - except: + except Exception: pass for d in devices: if ('interface_number' in d and d['interface_number'] == 0 diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index d34e8e8f6..cee5b1145 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -460,7 +460,7 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc multisig=multisig, ) return {'address': address} - except: + except Exception: pass raise BadArgumentError("No path supplied matched device keys") @@ -543,7 +543,7 @@ def toggle_passphrase(self): self._check_unlocked() try: device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) - except: + except Exception: if self.type == 'Keepkey': print('Confirm the action by entering your PIN', file=sys.stderr) print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index 94284f557..a802c32f2 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -149,7 +149,7 @@ def send_passphrase(passphrase=None, on_device=None): try: passphrase = self.ui.get_passphrase() - except: + except Exception: self.call_raw(messages.Cancel()) raise diff --git a/hwilib/devices/trezorlib/transport/webusb.py b/hwilib/devices/trezorlib/transport/webusb.py index 078e21f5e..5a718fb18 100644 --- a/hwilib/devices/trezorlib/transport/webusb.py +++ b/hwilib/devices/trezorlib/transport/webusb.py @@ -129,7 +129,7 @@ def enumerate(cls) -> Iterable["WebUsbTransport"]: # non-functional. dev.getProduct() devices.append(WebUsbTransport(dev)) - except: + except Exception: pass return devices diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 572041c80..e2293835c 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -31,7 +31,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface): break if found: break - except: + except Exception: pass time.sleep(0.5) # Cleanup diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 95198881b..b7e1a7961 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -28,7 +28,7 @@ def digitalbitbox_test_suite(simulator, rpc, userpass, interface): reply = send_plain(b'{"password":"0000"}', dev) if 'error' not in reply: break - except: + except Exception: pass time.sleep(0.5) # Cleanup From 1d0bbb62812787492434a1ad3f478f1f0cac5eba Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 3 Aug 2020 21:59:22 +0200 Subject: [PATCH 173/634] add bitbox02 dependency --- poetry.lock | 200 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + setup.py | 3 +- 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index f35a1d742..232ed0eed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,6 +17,62 @@ version = "1.5.2" [package.dependencies] pycodestyle = ">=2.5.0" +[[package]] +category = "main" +description = "Base58 and Base58Check implementation" +name = "base58" +optional = false +python-versions = ">=3.5" +version = "2.0.1" + +[[package]] +category = "main" +description = "Python library for bitbox02 communication" +name = "bitbox02" +optional = false +python-versions = ">=3.6" +version = "4.1.0" + +[package.dependencies] +base58 = ">=2.0.0" +ecdsa = ">=0.13" +hidapi = ">=0.7.99.post21" +noiseprotocol = ">=0.3" +protobuf = ">=3.7" +semver = ">=2.8.1" +typing-extensions = ">=3.7.4" + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.1" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.0" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +idna = ["idna (>=2.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + [[package]] category = "dev" description = "Python 2.7 backport of the \"dis\" module from Python 3.5+" @@ -125,6 +181,17 @@ version = "0.18" [package.dependencies] pbkdf2 = "*" +[[package]] +category = "main" +description = "Implementation of Noise Protocol Framework" +name = "noiseprotocol" +optional = false +python-versions = "~=3.5" +version = "0.3.1" + +[package.dependencies] +cryptography = ">=2.8" + [[package]] category = "main" description = "PKCS#5 v2.0 PBKDF2 Module" @@ -145,6 +212,18 @@ version = "2019.4.18" [package.dependencies] future = "*" +[[package]] +category = "main" +description = "Protocol Buffers" +name = "protobuf" +optional = false +python-versions = "*" +version = "3.12.4" + +[package.dependencies] +setuptools = "*" +six = ">=1.9" + [[package]] category = "main" description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" @@ -161,6 +240,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.6.0" +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + [[package]] category = "dev" description = "passive checker of Python programs" @@ -202,6 +289,14 @@ optional = false python-versions = "*" version = "0.2.0" +[[package]] +category = "main" +description = "Python helper for Semantic Versioning (http://semver.org/)" +name = "semver" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10.2" + [[package]] category = "main" description = "Python / C++ bindings helper module" @@ -210,6 +305,14 @@ optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" version = "5.14.2.1" +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + [[package]] category = "main" description = "Backported and Experimental Type Hints for Python 3.5+" @@ -235,7 +338,8 @@ testing = ["jaraco.itertools", "func-timeout"] qt = ["pyside2"] [metadata] -content-hash = "bce2677f0c45cb74f03eecfab2094577eed8fb16fe305a21c924128c4faa1b47" +content-hash = "fc39b2be42f870113feb38f56c177164fb7302eb07616d850f5a1b9e3e8509e1" +lock-version = "1.0" python-versions = "^3.6,<3.9" [metadata.files] @@ -246,6 +350,65 @@ altgraph = [ autopep8 = [ {file = "autopep8-1.5.2.tar.gz", hash = "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"}, ] +base58 = [ + {file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"}, + {file = "base58-2.0.1.tar.gz", hash = "sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79"}, +] +bitbox02 = [ + {file = "bitbox02-4.1.0-py3-none-any.whl", hash = "sha256:1af95952d67b74c80ccc0588e0aee983c764960da637bd24bc41a1cb89d5e127"}, + {file = "bitbox02-4.1.0.tar.gz", hash = "sha256:73a35594162f32897dd2b1880f0cfaa42922acd1c2d7f4cf3d94b8333329c931"}, +] +cffi = [ + {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, + {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, + {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, + {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, + {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, + {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, + {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, + {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, + {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, + {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, + {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, + {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, + {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, + {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, + {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, + {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, + {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, + {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, + {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, + {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, + {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, + {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, + {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, + {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, + {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, + {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, + {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, + {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, +] +cryptography = [ + {file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, + {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, + {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, + {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, + {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, + {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, + {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, + {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, + {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, + {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, + {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, + {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, + {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, + {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, + {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, + {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, + {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, + {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, + {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, +] dis3 = [ {file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"}, {file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"}, @@ -293,12 +456,35 @@ mccabe = [ mnemonic = [ {file = "mnemonic-0.18.tar.gz", hash = "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"}, ] +noiseprotocol = [ + {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, +] pbkdf2 = [ {file = "pbkdf2-1.3.tar.gz", hash = "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"}, ] pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, ] +protobuf = [ + {file = "protobuf-3.12.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3d59825cba9447e8f4fcacc1f3c892cafd28b964e152629b3f420a2fb5918b5a"}, + {file = "protobuf-3.12.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6009f3ebe761fad319b52199a49f1efa7a3729302947a78a3f5ea8e7e89e3ac2"}, + {file = "protobuf-3.12.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e2bd5c98952db3f1bb1af2e81b6a208909d3b8a2d32f7525c5cc10a6338b6593"}, + {file = "protobuf-3.12.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2becd0e238ae34caf96fa7365b87f65b88aebcf7864dfe5ab461c5005f4256d9"}, + {file = "protobuf-3.12.4-cp35-cp35m-win32.whl", hash = "sha256:ef991cbe34d7bb935ba6349406a210d3558b9379c21621c6ed7b99112af7350e"}, + {file = "protobuf-3.12.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a7b6cf201e67132ca99b8a6c4812fab541fdce1ceb54bb6f66bc336ab7259138"}, + {file = "protobuf-3.12.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4794a7748ee645d2ae305f3f4f0abd459e789c973b5bc338008960f83e0c554b"}, + {file = "protobuf-3.12.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f1796e0eb911bf5b08e76b753953effbeb6bc42c95c16597177f627eaa52c375"}, + {file = "protobuf-3.12.4-cp36-cp36m-win32.whl", hash = "sha256:c0c8d7c8f07eacd9e98a907941b56e57883cf83de069cfaeaa7e02c582f72ddb"}, + {file = "protobuf-3.12.4-cp36-cp36m-win_amd64.whl", hash = "sha256:2db6940c1914fa3fbfabc0e7c8193d9e18b01dbb4650acac249b113be3ba8d9e"}, + {file = "protobuf-3.12.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6842284bb15f1b19c50c5fd496f1e2a4cfefdbdfa5d25c02620cb82793295a7"}, + {file = "protobuf-3.12.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0b00429b87821f1e6f3d641327864e6f271763ae61799f7540bc58a352825fe2"}, + {file = "protobuf-3.12.4-cp37-cp37m-win32.whl", hash = "sha256:f10ba89f9cd508dc00e469918552925ef7cba38d101ca47af1e78f2f9982c6b3"}, + {file = "protobuf-3.12.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2636c689a6a2441da9a2ef922a21f9b8bfd5dfe676abd77d788db4b36ea86bee"}, + {file = "protobuf-3.12.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50b7bb2124f6a1fb0ddc6a44428ae3a21e619ad2cdf08130ac6c00534998ef07"}, + {file = "protobuf-3.12.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e77ca4e1403b363a88bde9e31c11d093565e925e1685f40b29385a52f2320794"}, + {file = "protobuf-3.12.4-py2.py3-none-any.whl", hash = "sha256:32f0bcdf85e0040f36b4f548c71177027f2a618cab00ba235197fa9e230b7289"}, + {file = "protobuf-3.12.4.tar.gz", hash = "sha256:c99e5aea75b6f2b29c8d8da5bdc5f5ed8d9a5b4f15115c8316a3f0a850f94656"}, +] pyaes = [ {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, ] @@ -306,6 +492,10 @@ pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, @@ -325,6 +515,10 @@ pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] +semver = [ + {file = "semver-2.10.2-py2.py3-none-any.whl", hash = "sha256:21e80ca738975ed513cba859db0a0d2faca2380aef1962f48272ebf9a8a44bd4"}, + {file = "semver-2.10.2.tar.gz", hash = "sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276"}, +] shiboken2 = [ {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d285d476a76f254bff69cc58c1d4385df295b42de1a818d4a8d11694c2d728fc"}, {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73d03e74f542204e351539e42ab3e3727a69408e1497af4c6e84fb66c3e706d8"}, @@ -333,6 +527,10 @@ shiboken2 = [ {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:fe4d0cf6737f1d01944be4cf3b401d74015c515ab84622bf04f47d64ffcd39f9"}, {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:c022203b7cf01df6ad0bb190d286c2965958243a16e47bee8c5e6bbb9d0cd475"}, ] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] typing-extensions = [ {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, diff --git a/pyproject.toml b/pyproject.toml index 4f976fdf0..55e0f70c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ mnemonic = "^0.18.0" typing-extensions = "^3.7" libusb1 = "^1.7" pyside2 = { version = "^5.14.0", optional = true } +bitbox02 = ">=4.1.0" [tool.poetry.extras] qt = ["pyside2"] diff --git a/setup.py b/setup.py index 70d8b1fd3..0d2116381 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,8 @@ 'libusb1>=1.7,<2.0', 'mnemonic>=0.18.0,<0.19.0', 'pyaes>=1.6,<2.0', - 'typing-extensions>=3.7,<4.0'] + 'typing-extensions>=3.7,<4.0', + 'bitbox02>=4.1.0'] extras_require = \ {'qt': ['pyside2>=5.14.0,<6.0.0']} From 4e4455d2b28eaa4037e3db64112b803a0fcf0edd Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 14 Jul 2020 15:51:39 +0200 Subject: [PATCH 174/634] add BitBox02 support --- README.md | 44 +-- docs/bitbox02.md | 49 +++ hwilib/commands.py | 11 +- hwilib/devices/__init__.py | 3 +- hwilib/devices/bitbox02.py | 613 ++++++++++++++++++++++++++++++ hwilib/gui.py | 49 ++- hwilib/udev/53-hid-bitbox02.rules | 1 + hwilib/udev/54-hid-bitbox02.rules | 1 + hwilib/ui/bitbox02pairing.ui | 120 ++++++ 9 files changed, 861 insertions(+), 30 deletions(-) create mode 100644 docs/bitbox02.md create mode 100644 hwilib/devices/bitbox02.py create mode 100644 hwilib/udev/53-hid-bitbox02.rules create mode 100644 hwilib/udev/54-hid-bitbox02.rules create mode 100644 hwilib/ui/bitbox02pairing.ui diff --git a/README.md b/README.md index 889036c23..555b9d111 100644 --- a/README.md +++ b/README.md @@ -90,29 +90,29 @@ The below table lists what devices and features are supported for each device. Please also see [docs](docs/) for additional information about each device. -| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard | +| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard | |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A | -| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A | -| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A | -| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes | -| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | -| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | -| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | -| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | -| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | -| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | +| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | +| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A | +| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes | +| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | +| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | +| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | +| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | +| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes | +| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes | ## Using with Bitcoin Core diff --git a/docs/bitbox02.md b/docs/bitbox02.md new file mode 100644 index 000000000..6949234dd --- /dev/null +++ b/docs/bitbox02.md @@ -0,0 +1,49 @@ +# BitBox02 + +The BitBox02 is supported by HWI. + +Current implemented commands are: + +* `signtx` +* `getxpub` +* `displayaddress` +* `setup` +* `wipe` +* `restore` +* `backup` +* `togglepassphrase` + +Multisig (P2WSH only) is supported by the BitBox02, but is not ingerated into HWI yet. Coming +soon^{tm}. + +# Usage Notes + +## Strict keypaths + +The BitBox02 has strict keypath validation. + +The only accepted keypaths for xpubs are: + +- `m/49'/0'/` for `p2wpkh-p2sh` (segwit wrapped in P2SH) +- `m/84'/0'/` for `p2wpkh` (native segwit v0) +- `m/48'/0'//2` for p2wsh multisig (native segwit v0 multisig). + +`account'` can be between `0'` and `99'`. + +For address keypaths, append `/0/
` for a receive and `/1/` for a change +address. Up to `10000` addresses are supported. + +In `--testnet` mode, the second element must be `1'` (e.g. `m/49'/1'/...`). + +## Signing with mixed input types + +The BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as +long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and +all account indexes are the same. + +Multisig and singlesig inputs cannot be mixed. + +## getmasterxpub and legacy addresses not supported + +`getmasterxpub` is the same as `getxpub` at the legacy keypath `m/44'/0'/0'`. Legacy xpub, addresses +and inputs are not supported. diff --git a/hwilib/commands.py b/hwilib/commands.py index b7ff7ec7b..4413a64b4 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -9,6 +9,7 @@ from .base58 import xpub_to_pub_hex from .errors import ( UnknownDeviceError, + UnavailableActionError, BAD_ARGUMENT, NOT_IMPLEMENTED, ) @@ -206,10 +207,12 @@ def getdescriptors(client, account=0): for internal in [False, True]: descriptors = [] - desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.PKH, account=account) - desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.SH_WPKH, account=account) - desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.WPKH, account=account) - for desc in [desc1, desc2, desc3]: + for addr_type in (AddressType.PKH, AddressType.SH_WPKH, AddressType.WPKH): + try: + desc = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=addr_type, account=account) + except UnavailableActionError: + # Device does not support this address type or network. Skip. + continue if not isinstance(desc, Descriptor): return desc descriptors.append(desc.serialize()) diff --git a/hwilib/devices/__init__.py b/hwilib/devices/__init__.py index a4f93f114..43188c9b3 100644 --- a/hwilib/devices/__init__.py +++ b/hwilib/devices/__init__.py @@ -3,5 +3,6 @@ 'ledger', 'keepkey', 'digitalbitbox', - 'coldcard' + 'coldcard', + 'bitbox02', ] diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py new file mode 100644 index 000000000..5b81c4a77 --- /dev/null +++ b/hwilib/devices/bitbox02.py @@ -0,0 +1,613 @@ +from typing import ( + cast, + Any, + Callable, + Dict, + Optional, + Union, + Tuple, + List, + Sequence, + TypeVar, +) +from binascii import unhexlify +import struct +import builtins +import sys +from functools import wraps + +from ..hwwclient import HardwareWalletClient, Descriptor +from ..serializations import ( + PSBT, + CTxOut, + is_p2pkh, + is_p2wpkh, + is_p2wsh, + ser_uint256, + ser_sig_der, +) +from ..errors import ( + HWWError, + ActionCanceledError, + BadArgumentError, + DeviceNotReadyError, + UnavailableActionError, + DEVICE_NOT_INITIALIZED, + handle_errors, + common_err_msgs, +) + +import hid # type: ignore + +from .trezorlib.tools import parse_path + +from bitbox02 import util +from bitbox02 import bitbox02 +from bitbox02.communication import ( + devices, + u2fhid, + FirmwareVersionOutdatedException, + Bitbox02Exception, + UserAbortException, + HARDENED, + ERR_GENERIC, +) + +from bitbox02.communication.bitbox_api_protocol import ( + Platform, + BitBox02Edition, + BitBoxNoiseConfig, +) + + +class BitBox02Error(UnavailableActionError): + def __init__(self, msg: str): + """ + BitBox02 unexpected error. The BitBox02 does not return give granular error messages, + so we give hints to as what could be wrong. + """ + msg = "Input error: {}. A keypath might be invalid. Supported keypaths are: ".format( + msg + ) + msg += "m/49'/0'/ for p2wpkh-p2sh; " + msg += "m/84'/0'/ for p2wpkh; " + msg += "m/48'/0'//2' for p2wsh multisig; " + msg += "account can be between 0' and 99'; " + msg += "For address keypaths, append /0/
for a receive and /1/ for a change address." + super().__init__(msg) + + +ERR_INVALID_INPUT = 101 + +PURPOSE_P2WPKH_P2SH = 49 + HARDENED +PURPOSE_P2WPKH = 84 + HARDENED +PURPOSE_MULTISIG_P2WSH = 48 + HARDENED + +# External GUI tools using hwi.py as a command line tool to integrate hardware wallets usually do +# not have an actual terminal for IO. +_using_external_gui = not sys.stdout.isatty() +if _using_external_gui: + _unpaired_errmsg = "Device not paired yet. Please pair using the BitBoxApp, then close the BitBoxApp and try again." +else: + _unpaired_errmsg = "Device not paired yet. Please use any subcommand to pair" + + +class SilentNoiseConfig(util.BitBoxAppNoiseConfig): + """ + Used during `enumerate()`. Raises an exception if the device is unpaired. + Attestation check is silent. + + Rationale: enumerate() should not show any dialogs. + """ + + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + raise DeviceNotReadyError(_unpaired_errmsg) + + def attestation_check(self, result: bool) -> None: + pass + + +class CLINoiseConfig(util.BitBoxAppNoiseConfig): + """ Noise pairing and attestation check handling in the terminal (stdin/stdout) """ + + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + if _using_external_gui: + # The user can't see the pairing in the terminal. The + # output format is also not appropriate for parsing by + # external tools doing inter process communication using + # stdin/stdout. For now, we direct the user to pair in the + # BitBoxApp instead. + raise DeviceNotReadyError(_unpaired_errmsg) + + print("Please compare and confirm the pairing code on your BitBox02:") + print(code) + if not device_response(): + return False + return input("Accept pairing? [y]/n: ").strip() != "n" + + def attestation_check(self, result: bool) -> None: + if result: + sys.stderr.write("BitBox02 attestation check PASSED\n") + else: + sys.stderr.write("BitBox02 attestation check FAILED\n") + sys.stderr.write( + "Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.\n" + ) + + +def enumerate(password: str = "") -> List[Dict[str, object]]: + """ + Enumerate all BitBox02 devices. Bootloaders excluded. + """ + result = [] + for device_info in devices.get_any_bitbox02s(): + path = device_info["path"].decode() + client = Bitbox02Client(path) + client.set_noise_config(SilentNoiseConfig()) + version, platform, edition, unlocked = bitbox02.BitBox02.get_info( + client.transport + ) + if platform != Platform.BITBOX02: + client.close() + continue + if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY): + client.close() + continue + + assert isinstance(edition, BitBox02Edition) + + d_data = { + "type": "bitbox02", + "path": path, + "model": { + BitBox02Edition.MULTI: "bitbox02_multi", + BitBox02Edition.BTCONLY: "bitbox02_btconly", + }[edition], + "needs_pin_sent": False, + "needs_passphrase_sent": False, + } + + with handle_errors(common_err_msgs["enumerate"], d_data): + if not unlocked: + raise DeviceNotReadyError( + "Please load wallet to unlock." + if _using_external_gui + else "Please use any subcommand to unlock" + ) + bb02 = client.init() + info = bb02.device_info() + if not info["initialized"]: + raise HWWError("Not initialized", DEVICE_NOT_INITIALIZED) + d_data["fingerprint"] = client.get_master_fingerprint_hex() + + result.append(d_data) + + client.close() + return result + + +T = TypeVar("T", bound=Callable[..., Any]) + + +def bitbox02_exception(f: T) -> T: + """ + Maps bitbox02 library exceptions into a HWI exceptions. + """ + + @wraps(f) + def func(*args, **kwargs): # type: ignore + """ Wraps f, mapping exceptions. """ + try: + return f(*args, **kwargs) + except UserAbortException: + raise ActionCanceledError("{} canceled".format(f.__name__)) + except Bitbox02Exception as exc: + if exc.code in (ERR_GENERIC, ERR_INVALID_INPUT): + raise BitBox02Error(str(exc)) + raise exc + except FirmwareVersionOutdatedException as exc: + raise DeviceNotReadyError(str(exc)) + + return cast(T, func) + + +# This class extends the HardwareWalletClient for BitBox02 specific things +class Bitbox02Client(HardwareWalletClient): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: + """ + Initializes a new BitBox02 client instance. + """ + super().__init__(path, password=password, expert=expert) + if password != "": + raise BadArgumentError( + "The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock." + ) + + hid_device = hid.device() + hid_device.open_path(path.encode()) + self.transport = u2fhid.U2FHid(hid_device) + self.device_path = path + + # use self.init() to access self.bb02. + self.bb02: Optional[bitbox02.BitBox02] = None + + self.noise_config: BitBoxNoiseConfig = CLINoiseConfig() + + def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None: + self.noise_config = noise_config + + def init(self) -> bitbox02.BitBox02: + if self.bb02 is not None: + return self.bb02 + + for device_info in devices.get_any_bitbox02s(): + if device_info["path"].decode() == self.device_path: + bb02 = bitbox02.BitBox02( + transport=self.transport, + device_info=device_info, + noise_config=self.noise_config, + ) + try: + bb02.check_min_version() + except FirmwareVersionOutdatedException as exc: + sys.stderr.write("WARNING: {}\n".format(exc)) + raise + self.bb02 = bb02 + return bb02 + raise Exception( + "Could not find the hid device info for path {}".format(self.device_path) + ) + + def close(self) -> None: + self.transport.close() + + def get_master_fingerprint_hex(self) -> str: + """ + HWI by default retrieves the fingerprint at m/ by getting the xpub at m/0', which contains the parent fingerprint. + The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/. + """ + bb02 = self.init() + if not bb02.device_info()["initialized"]: + raise UnavailableActionError("Not initialized") + return bb02.root_fingerprint().hex() + + def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: + raise UnavailableActionError( + "The BitBox02 does not need a PIN sent from the host" + ) + + def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: + raise UnavailableActionError( + "The BitBox02 does not need a PIN sent from the host" + ) + + def _get_coin(self) -> bitbox02.btc.BTCCoin: + if self.is_testnet: + return bitbox02.btc.TBTC + return bitbox02.btc.BTC + + def _get_xpub(self, keypath: Sequence[int]) -> str: + xpub_type = ( + bitbox02.btc.BTCPubRequest.TPUB + if self.is_testnet + else bitbox02.btc.BTCPubRequest.XPUB + ) + return self.init().btc_xpub( + keypath, coin=self._get_coin(), xpub_type=xpub_type, display=False + ) + + def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: + path_uint32s = parse_path(bip32_path) + try: + xpub = self._get_xpub(path_uint32s) + except Bitbox02Exception as exc: + raise BitBox02Error(str(exc)) + return {"xpub": xpub} + + @bitbox02_exception + def display_address( + self, + bip32_path: str, + p2sh_p2wpkh: bool, + bech32: bool, + redeem_script: Optional[str] = None, + descriptor: Optional[Descriptor] = None, + ) -> Dict[str, str]: + if redeem_script: + raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") + + if p2sh_p2wpkh: + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ) + elif bech32: + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ) + else: + raise UnavailableActionError( + "The BitBox02 does not support legacy p2pkh addresses" + ) + address = self.init().btc_address( + parse_path(bip32_path), + coin=self._get_coin(), + script_config=script_config, + display=True, + ) + return {"address": address} + + @bitbox02_exception + def sign_tx(self, psbt: PSBT) -> Dict[str, str]: + def find_our_key( + keypaths: Dict[bytes, Sequence[int]] + ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: + """ + Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey. + Returns the pubkey and the keypath, without the fingerprint. + """ + for pubkey, keypath_with_fingerprint in keypaths.items(): + fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:] + # Cheap check if the key is ours. + if fp != master_fp: + continue + + # Expensive check if the key is ours. + # TODO: check for fingerprint collision + # keypath_account = keypath[:-2] + + return pubkey, keypath + return None, None + + def get_simple_type( + output: CTxOut, redeem_script: bytes + ) -> bitbox02.btc.BTCScriptConfig.SimpleType: + if is_p2pkh(output.scriptPubKey): + raise BadArgumentError( + "The BitBox02 does not support legacy p2pkh scripts" + ) + if is_p2wpkh(output.scriptPubKey): + return bitbox02.btc.BTCScriptConfig.P2WPKH + if output.is_p2sh() and is_p2wpkh(redeem_script): + return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + raise BadArgumentError( + "Input script type not recognized of input {}.".format(input_index) + ) + + master_fp = struct.unpack(" Dict[str, str]: + raise UnavailableActionError("The BitBox02 does not support 'signmessage'") + + @bitbox02_exception + def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: + bb02 = self.init() + info = bb02.device_info() + if info["mnemonic_passphrase_enabled"]: + bb02.disable_mnemonic_passphrase() + else: + bb02.enable_mnemonic_passphrase() + return {"success": True} + + @bitbox02_exception + def setup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + if passphrase: + raise UnavailableActionError( + "Passphrase not needed when setting up a BitBox02." + ) + + bb02 = self.init() + if bb02.device_info()["initialized"]: + raise UnavailableActionError("The BitBox02 must be wiped before setup.") + + if label: + bb02.set_device_name(label) + if not bb02.set_password(): + return {"success": False} + return {"success": bb02.create_backup()} + + @bitbox02_exception + def wipe_device(self) -> Dict[str, Union[bool, str, int]]: + return {"success": self.init().reset()} + + @bitbox02_exception + def backup_device( + self, label: str = "", passphrase: str = "" + ) -> Dict[str, Union[bool, str, int]]: + if label or passphrase: + raise UnavailableActionError( + "Label/passphrase not needed when exporting mnemonic from the BitBox02." + ) + + return {"success": self.init().show_mnemonic()} + + @bitbox02_exception + def restore_device( + self, label: str = "", word_count: int = 24 + ) -> Dict[str, Union[bool, str, int]]: + bb02 = self.init() + if bb02.device_info()["initialized"]: + raise UnavailableActionError("The BitBox02 must be wiped before restore.") + + if label: + bb02.set_device_name(label) + + return {"success": bb02.restore_from_mnemonic()} diff --git a/hwilib/gui.py b/hwilib/gui.py index dcdfeabf0..f0c213507 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -3,12 +3,14 @@ import json import logging import sys +from typing import Callable from . import commands, __version__ from .cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED try: + from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog from .ui.ui_getxpubdialog import Ui_GetXpubDialog from .ui.ui_getkeypooloptionsdialog import Ui_GetKeypoolOptionsDialog @@ -23,7 +25,9 @@ from PySide2.QtGui import QRegExpValidator from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QMainWindow -from PySide2.QtCore import QRegExp, Signal, Slot +from PySide2.QtCore import QCoreApplication, QRegExp, Signal, Slot + +import bitbox02.util def do_command(f, *args, **kwargs): result = {} @@ -207,6 +211,41 @@ def toggle_account(self, checked): self.ui.path_lineedit.setEnabled(True) self.ui.account_spinbox.setEnabled(False) +class BitBox02PairingDialog(QDialog): + def __init__(self, pairing_code: str, device_response: Callable[[], bool]): + super(BitBox02PairingDialog, self).__init__() + self.ui = Ui_BitBox02PairingDialog() + self.ui.setupUi(self) + self.setWindowTitle('Verify BitBox02 pairing code') + self.ui.pairingCode.setText(pairing_code.replace("\n", "
")) + self.ui.buttonBox.setEnabled(False) + self.device_response = device_response + + def enable_buttons(self): + self.ui.buttonBox.setEnabled(True) + +class BitBox02NoiseConfig(bitbox02.util.BitBoxAppNoiseConfig): + """ GUI elements to perform the BitBox02 pairing and attestatoin check """ + + def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: + dialog = BitBox02PairingDialog(code, device_response) + dialog.show() + # render the window since the next operation is blocking + QCoreApplication.processEvents() + if not device_response(): + return False + dialog.enable_buttons() + dialog.exec_() + return dialog.result() == QDialog.Accepted + + def attestation_check(self, result: bool) -> None: + if not result: + QMessageBox.warning( + None, + "BitBox02 attestation check", + "BitBox02 attestation check failed. Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.", + ) + class HWIQt(QMainWindow): def __init__(self, passphrase='', testnet=False): super(HWIQt, self).__init__() @@ -293,7 +332,6 @@ def get_client_and_device_info(self, index): self.ui.getxpub_button.setEnabled(True) self.ui.signtx_button.setEnabled(True) - self.ui.signmsg_button.setEnabled(True) self.ui.display_addr_button.setEnabled(True) self.ui.getkeypool_opts_button.setEnabled(True) @@ -302,7 +340,12 @@ def get_client_and_device_info(self, index): self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) self.client.is_testnet = self.testnet - self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] == 'trezor' or self.device_info['type'] == 'keepkey') + if self.device_info['type'] == 'bitbox02': + self.client.set_noise_config(BitBox02NoiseConfig()) + + self.ui.setpass_button.setEnabled(self.device_info['type'] != 'bitbox02') + self.ui.signmsg_button.setEnabled(self.device_info['type'] != 'bitbox02') + self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] in ('trezor', 'keepkey', 'bitbox02', )) self.get_device_info() diff --git a/hwilib/udev/53-hid-bitbox02.rules b/hwilib/udev/53-hid-bitbox02.rules new file mode 100644 index 000000000..2daffc03b --- /dev/null +++ b/hwilib/udev/53-hid-bitbox02.rules @@ -0,0 +1 @@ +SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403" diff --git a/hwilib/udev/54-hid-bitbox02.rules b/hwilib/udev/54-hid-bitbox02.rules new file mode 100644 index 000000000..1b74e4774 --- /dev/null +++ b/hwilib/udev/54-hid-bitbox02.rules @@ -0,0 +1 @@ +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n" diff --git a/hwilib/ui/bitbox02pairing.ui b/hwilib/ui/bitbox02pairing.ui new file mode 100644 index 000000000..6b1a1997a --- /dev/null +++ b/hwilib/ui/bitbox02pairing.ui @@ -0,0 +1,120 @@ + + + BitBox02PairingDialog + + + Qt::WindowModal + + + + 0 + 0 + 400 + 209 + + + + Dialog + + + + + 30 + 160 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::No|QDialogButtonBox::Yes + + + + + + 20 + 80 + 331 + 61 + + + + + DejaVu Sans Mono + 15 + 75 + true + + + + + + + Qt::RichText + + + Qt::AlignCenter + + + + + + 20 + 10 + 351 + 61 + + + + + 11 + + + + Please verify the pairing code matches what is +shown on your BitBox02. + + + Qt::PlainText + + + + + + + buttonBox + accepted() + BitBox02PairingDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + BitBox02PairingDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From ed1125115fe8feea974b94f914c042b07d4786c9 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 25 Jul 2020 13:21:32 +0200 Subject: [PATCH 175/634] typecheck bitbox02.py Since trezorlib is imported also by bitbox02, it would be type-checked by mypy. Since it contains no types, it is easiest to ignore trezorlib in mypy. --implicit-reexport is disabled by --strict, but that is a bit too strict. The bitbox02 package relies on implicit reexports. --- .travis.yml | 4 +++- mypy.ini | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 mypy.ini diff --git a/.travis.yml b/.travis.yml index 33e9d8113..a392623a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,9 @@ jobs: stage: lint install: - pip install mypy - script: mypy --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py + - pip install poetry + - poetry install + script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py - name: Run non-device tests only stage: test install: diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..9235ac7f2 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +# Do not type check trezorlib because it does not provide types. +[mypy-hwilib.devices.trezorlib.*] +follow_imports = skip From 4df060d0c6a6ceddecc06713ac96b8d5f5be0e94 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sun, 16 Aug 2020 12:19:33 +0200 Subject: [PATCH 176/634] bitbox02: use local copy of parse_path Device libraries should not cross-reference each other and stay independent. --- hwilib/devices/bitbox02.py | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 5b81c4a77..6bc22db8e 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -39,8 +39,6 @@ import hid # type: ignore -from .trezorlib.tools import parse_path - from bitbox02 import util from bitbox02 import bitbox02 from bitbox02.communication import ( @@ -135,6 +133,40 @@ def attestation_check(self, result: bool) -> None: ) +def _parse_path(nstr: str) -> Sequence[int]: + """ + Adapted from trezorlib.tools.parse_path. + Convert BIP32 path string to list of uint32 integers with hardened flags. + Several conventions are supported to set the hardened flag: -1, 1', 1h + + e.g.: "0/1h/1" -> [0, 0x80000001, 1] + + :param nstr: path string + :return: list of integers + """ + if not nstr: + return [] + + n = nstr.split("/") + + # m/a/b/c => a/b/c + if n[0] == "m": + n = n[1:] + + def str_to_harden(x: str) -> int: + if x.startswith("-"): + return abs(int(x)) + HARDENED + elif x.endswith(("h", "'")): + return int(x[:-1]) + HARDENED + else: + return int(x) + + try: + return [str_to_harden(x) for x in n] + except Exception: + raise ValueError("Invalid BIP32 path", nstr) + + def enumerate(password: str = "") -> List[Dict[str, object]]: """ Enumerate all BitBox02 devices. Bootloaders excluded. @@ -297,7 +329,7 @@ def _get_xpub(self, keypath: Sequence[int]) -> str: ) def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: - path_uint32s = parse_path(bip32_path) + path_uint32s = _parse_path(bip32_path) try: xpub = self._get_xpub(path_uint32s) except Bitbox02Exception as exc: @@ -329,7 +361,7 @@ def display_address( "The BitBox02 does not support legacy p2pkh addresses" ) address = self.init().btc_address( - parse_path(bip32_path), + _parse_path(bip32_path), coin=self._get_coin(), script_config=script_config, display=True, From d255229639117914582442bf780cb9d4dab4d59b Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 26 Aug 2020 15:39:26 +0200 Subject: [PATCH 177/634] commands/find_device: only get the device fingerprint if required If the fingerprint is not specified on the CLI, the fingerprint does not need to be queried from the device. This is helpful for uninitialized devices, to invoke the 'setup' or 'restore' commands, at which point the device does not have a keystore and fingerprint yet. --- hwilib/commands.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 4413a64b4..8ffcd3020 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -62,13 +62,14 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False): try: client = get_client(d['type'], d['path'], password, expert) - master_fpr = d.get('fingerprint', None) - if master_fpr is None: - master_fpr = client.get_master_fingerprint_hex() - - if fingerprint and master_fpr != fingerprint: - client.close() - continue + if fingerprint: + master_fpr = d.get('fingerprint', None) + if master_fpr is None: + master_fpr = client.get_master_fingerprint_hex() + + if master_fpr != fingerprint: + client.close() + continue return client except Exception: if client: From 6169788aa7057b725599a6e8264d8d6eaa3a5e10 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 26 Aug 2020 16:36:51 +0200 Subject: [PATCH 178/634] bitbox02: consistent errs when the device is (not) initialized Running e.g. getxpub when the device is not initialized results in the device returning with 'BAD_STATE', which is not so user friendly. --- hwilib/devices/bitbox02.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 6bc22db8e..04e7036f9 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -206,10 +206,6 @@ def enumerate(password: str = "") -> List[Dict[str, object]]: if _using_external_gui else "Please use any subcommand to unlock" ) - bb02 = client.init() - info = bb02.device_info() - if not info["initialized"]: - raise HWWError("Not initialized", DEVICE_NOT_INITIALIZED) d_data["fingerprint"] = client.get_master_fingerprint_hex() result.append(d_data) @@ -268,7 +264,7 @@ def __init__(self, path: str, password: str = "", expert: bool = False) -> None: def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None: self.noise_config = noise_config - def init(self) -> bitbox02.BitBox02: + def init(self, expect_initialized: bool = True) -> bitbox02.BitBox02: if self.bb02 is not None: return self.bb02 @@ -285,6 +281,16 @@ def init(self) -> bitbox02.BitBox02: sys.stderr.write("WARNING: {}\n".format(exc)) raise self.bb02 = bb02 + is_initialized = bb02.device_info()["initialized"] + if expect_initialized: + if not is_initialized: + raise HWWError( + "The BitBox02 must be initialized first.", + DEVICE_NOT_INITIALIZED, + ) + elif is_initialized: + raise UnavailableActionError("The BitBox02 must be wiped before setup.") + return bb02 raise Exception( "Could not find the hid device info for path {}".format(self.device_path) @@ -299,8 +305,6 @@ def get_master_fingerprint_hex(self) -> str: The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/. """ bb02 = self.init() - if not bb02.device_info()["initialized"]: - raise UnavailableActionError("Not initialized") return bb02.root_fingerprint().hex() def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: @@ -606,9 +610,7 @@ def setup_device( "Passphrase not needed when setting up a BitBox02." ) - bb02 = self.init() - if bb02.device_info()["initialized"]: - raise UnavailableActionError("The BitBox02 must be wiped before setup.") + bb02 = self.init(expect_initialized=False) if label: bb02.set_device_name(label) @@ -635,9 +637,7 @@ def backup_device( def restore_device( self, label: str = "", word_count: int = 24 ) -> Dict[str, Union[bool, str, int]]: - bb02 = self.init() - if bb02.device_info()["initialized"]: - raise UnavailableActionError("The BitBox02 must be wiped before restore.") + bb02 = self.init(expect_initialized=False) if label: bb02.set_device_name(label) From 5010844a2766c504a02e7f13cd02524588bbe627 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 26 Aug 2020 15:59:26 +0200 Subject: [PATCH 179/634] hwwclient: fix send_pin signature Was missing the pin argument. --- hwilib/hwwclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 98de2995c..e27db11f9 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -162,7 +162,7 @@ def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def send_pin(self) -> Dict[str, Union[bool, str, int]]: + def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: """Send PIN. Must return a dictionary with the "success" key, From 2f56cd383991aa3a92e0893cb93dd2090fd5c3ce Mon Sep 17 00:00:00 2001 From: fametrano Date: Thu, 27 Aug 2020 11:23:54 +0200 Subject: [PATCH 180/634] ignored .vscode folder --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 843b1be04..c97e1b36e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ venv/ ENV/ env.bak/ venv.bak/ + +.vscode From b24f2aaa2debbe90bf102fb96c778733ce81259b Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 31 Aug 2020 13:02:07 +0200 Subject: [PATCH 181/634] hwilib/gui: make sure the bb02 pairing window is painted Sometimes, one processEvents() was not enough to have Qt5 finish painting the window before proceeding onto the blocking call to show the pairing code on the bitbox02 screen. This is an attempt at making this more robust. --- hwilib/gui.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index f0c213507..afb6c8328 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -3,6 +3,7 @@ import json import logging import sys +import time from typing import Callable from . import commands, __version__ @@ -220,6 +221,11 @@ def __init__(self, pairing_code: str, device_response: Callable[[], bool]): self.ui.pairingCode.setText(pairing_code.replace("\n", "
")) self.ui.buttonBox.setEnabled(False) self.device_response = device_response + self.painted = False + + def paintEvent(self, ev): + super().paintEvent(ev) + self.painted = True def enable_buttons(self): self.ui.buttonBox.setEnabled(True) @@ -231,7 +237,11 @@ def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool: dialog = BitBox02PairingDialog(code, device_response) dialog.show() # render the window since the next operation is blocking - QCoreApplication.processEvents() + while True: + QCoreApplication.processEvents() + if dialog.painted: + break + time.sleep(0.1) if not device_response(): return False dialog.enable_buttons() From 0683dc246ad8c828ac0b14d99510bdea293f4a66 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 1 Sep 2020 05:15:38 +0200 Subject: [PATCH 182/634] Fix README features table rendering --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 555b9d111..f64e4a7aa 100644 --- a/README.md +++ b/README.md @@ -91,15 +91,15 @@ The below table lists what devices and features are supported for each device. Please also see [docs](docs/) for additional information about each device. | Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard | -|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | | Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | | Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | -| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A | -| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes | +| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A | +| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes | | P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | | P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | From 7ba2f61c3238664e38fddcb037dcd0e8e8b5c7b2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 16 Aug 2020 12:28:35 -0400 Subject: [PATCH 183/634] Move ExtendedKey to key.py --- hwilib/devices/coldcard.py | 4 ++- hwilib/devices/digitalbitbox.py | 4 ++- hwilib/devices/ledger.py | 5 ++- hwilib/devices/trezor.py | 5 ++- hwilib/key.py | 64 +++++++++++++++++++++++++++++++++ hwilib/serializations.py | 53 --------------------------- 6 files changed, 78 insertions(+), 57 deletions(-) create mode 100644 hwilib/key.py diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 8ef0de799..fda6096d7 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -39,8 +39,10 @@ get_xpub_fingerprint, xpub_main_2_test, ) -from ..serializations import ( +from ..key import ( ExtendedKey, +) +from ..serializations import ( PSBT, ) from hashlib import sha256 diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 70ef6c5f7..d9f694854 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -28,9 +28,11 @@ common_err_msgs, handle_errors, ) +from ..key import ( + ExtendedKey, +) from ..serializations import ( CTransaction, - ExtendedKey, hash256, is_p2pk, is_p2pkh, diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 1f9eb9dbd..de0cf0951 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -24,8 +24,11 @@ import hid import struct from .. import base58 -from ..serializations import ( + +from ..key import ( ExtendedKey, +) +from ..serializations import ( hash256, hash160, is_p2sh, diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index cee5b1145..0d1475b1d 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -44,9 +44,12 @@ to_address, xpub_main_2_test, ) + +from ..key import ( + ExtendedKey, +) from ..serializations import ( CTxOut, - ExtendedKey, is_p2pkh, is_p2sh, is_p2wsh, diff --git a/hwilib/key.py b/hwilib/key.py new file mode 100644 index 000000000..6a3c29900 --- /dev/null +++ b/hwilib/key.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The HWI developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from . import base58 + +import binascii +import struct +from typing import ( + Dict, +) + +# An extended public key (xpub) or private key (xprv). Just a data container for now. +# Only handles deserialization of extended keys into component data to be handled by something else +class ExtendedKey(object): + + MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' + MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' + TESTNET_PUBLIC = b'\x04\x35\x87\xCF' + TESTNET_PRIVATE = b'\x04\x35\x83\x94' + + def __init__(self) -> None: + self.is_testnet = False + self.is_private = False + self.depth = 0 + self.parent_fingerprint = b'' + self.child_num = 0 + self.chaincode = b'' + self.pubkey = b'' + self.privkey = b'' + + def deserialize(self, xpub: str) -> None: + data = base58.decode(xpub)[:-4] # Decoded xpub without checksum + + version = data[0:4] + if version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE: + self.is_testnet = True + if version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE: + self.is_private = True + + self.depth = data[4] + self.parent_fingerprint = data[5:9] + self.child_num = struct.unpack('>I', data[9:13])[0] + self.chaincode = data[13:45] + + if self.is_private: + self.privkey = data[46:] + else: + self.pubkey = data[45:78] + + def get_printable_dict(self) -> Dict[str, object]: + d: Dict[str, object] = {} + d['testnet'] = self.is_testnet + d['private'] = self.is_private + d['depth'] = self.depth + d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode() + d['child_num'] = self.child_num + d['chaincode'] = binascii.hexlify(self.chaincode).decode() + if self.is_private: + d['privkey'] = binascii.hexlify(self.privkey).decode() + else: + d['pubkey'] = binascii.hexlify(self.pubkey).decode() + return d diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 228558520..773aa4411 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -17,7 +17,6 @@ from io import BytesIO, BufferedReader from .errors import PSBTSerializationError -from . import base58 import struct import binascii @@ -870,55 +869,3 @@ def serialize(self) -> str: # return hex string return HexToBase64(r.hex()).decode() - -# An extended public key (xpub) or private key (xprv). Just a data container for now. -# Only handles deserialization of extended keys into component data to be handled by something else -class ExtendedKey(object): - - MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' - MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' - TESTNET_PUBLIC = b'\x04\x35\x87\xCF' - TESTNET_PRIVATE = b'\x04\x35\x83\x94' - - def __init__(self) -> None: - self.is_testnet = False - self.is_private = False - self.depth = 0 - self.parent_fingerprint = b'' - self.child_num = 0 - self.chaincode = b'' - self.pubkey = b'' - self.privkey = b'' - - def deserialize(self, xpub: str) -> None: - data = base58.decode(xpub)[:-4] # Decoded xpub without checksum - - version = data[0:4] - if version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE: - self.is_testnet = True - if version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE: - self.is_private = True - - self.depth = data[4] - self.parent_fingerprint = data[5:9] - self.child_num = struct.unpack('>I', data[9:13])[0] - self.chaincode = data[13:45] - - if self.is_private: - self.privkey = data[46:] - else: - self.pubkey = data[45:78] - - def get_printable_dict(self) -> Dict[str, object]: - d: Dict[str, object] = {} - d['testnet'] = self.is_testnet - d['private'] = self.is_private - d['depth'] = self.depth - d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode() - d['child_num'] = self.child_num - d['chaincode'] = binascii.hexlify(self.chaincode).decode() - if self.is_private: - d['privkey'] = binascii.hexlify(self.privkey).decode() - else: - d['pubkey'] = binascii.hexlify(self.pubkey).decode() - return d From 5ed8ab01435f39381e92be7381579d3f7edca2e7 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 16 Aug 2020 12:36:21 -0400 Subject: [PATCH 184/634] Type check ExtendedKey --- .travis.yml | 2 +- hwilib/key.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index a392623a9..675108d7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,7 +70,7 @@ jobs: - pip install mypy - pip install poetry - poetry install - script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py + script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py hwilib/key.py - name: Run non-device tests only stage: test install: diff --git a/hwilib/key.py b/hwilib/key.py index 6a3c29900..53bc79431 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -21,14 +21,14 @@ class ExtendedKey(object): TESTNET_PRIVATE = b'\x04\x35\x83\x94' def __init__(self) -> None: - self.is_testnet = False - self.is_private = False - self.depth = 0 - self.parent_fingerprint = b'' - self.child_num = 0 - self.chaincode = b'' - self.pubkey = b'' - self.privkey = b'' + self.is_testnet: bool = False + self.is_private: bool = False + self.depth: int = 0 + self.parent_fingerprint: bytes = b'' + self.child_num: int = 0 + self.chaincode: bytes = b'' + self.pubkey: bytes = b'' + self.privkey: bytes = b'' def deserialize(self, xpub: str) -> None: data = base58.decode(xpub)[:-4] # Decoded xpub without checksum From 06cba7e9f28c3f32c5ce999c727157ff18352953 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 13:54:08 -0400 Subject: [PATCH 185/634] Add KeyOriginInfo class --- hwilib/key.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 53bc79431..30289e745 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -9,8 +9,11 @@ import struct from typing import ( Dict, + Sequence, ) +HARDENED_FLAG = 1 << 31 + # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): @@ -62,3 +65,53 @@ def get_printable_dict(self) -> Dict[str, object]: else: d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d + + +class KeyOriginInfo(object): + def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: + self.fingerprint: bytes = fingerprint + self.path: Sequence[int] = path + + @classmethod + def deserialize(cls, s: bytes) -> object: + """ + Deserialize a serialized KeyOriginInfo. + They will be serialized in the same way that PSBTs serialize derivation paths + """ + fingerprint = s[0:4] + s = s[4:] + path = list(struct.unpack("<" + "I" * (len(s) // 4), s)) + return cls(fingerprint, path) + + def serialize(self) -> bytes: + """ + Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs + """ + r = self.fingerprint + r += struct.pack("<" + "I" * len(self.path), *self.path) + return r + + def _path_string(self) -> str: + s = "" + for i in self.path: + hardened = i & HARDENED_FLAG != 0 + i &= ~HARDENED_FLAG + s += "/" + str(i) + if hardened: + s += "'" + return s + + def to_string(self) -> str: + """ + Return the KeyOriginInfo as a string in the form ///... + This is the same way that KeyOriginInfo is shown in descriptors + """ + s = binascii.hexlify(self.fingerprint).decode() + s += self._path_string() + return s + + def get_derivation_path(self) -> str: + """ + Return the string for just the path + """ + return "m" + self._path_string() From a3a7c8f8bfbb4c6254959b42d4fb26bb379764ec Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 14:01:12 -0400 Subject: [PATCH 186/634] Add parse_path from trezorlib to key.py --- hwilib/key.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 30289e745..6d3976e7e 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -14,6 +14,14 @@ HARDENED_FLAG = 1 << 31 + +def H_(x: int) -> int: + """ + Shortcut function that "hardens" a number in a BIP44 path. + """ + return x | HARDENED_FLAG + + # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): @@ -115,3 +123,36 @@ def get_derivation_path(self) -> str: Return the string for just the path """ return "m" + self._path_string() + + +def parse_path(nstr: str) -> Sequence[int]: + """ + Convert BIP32 path string to list of uint32 integers with hardened flags. + Several conventions are supported to set the hardened flag: -1, 1', 1h + + e.g.: "0/1h/1" -> [0, 0x80000001, 1] + + :param nstr: path string + :return: list of integers + """ + if not nstr: + return [] + + n = nstr.split("/") + + # m/a/b/c => a/b/c + if n[0] == "m": + n = n[1:] + + def str_to_harden(x: str) -> int: + if x.startswith("-"): + return H_(abs(int(x))) + elif x.endswith(("h", "'")): + return H_(int(x[:-1])) + else: + return int(x) + + try: + return [str_to_harden(x) for x in n] + except Exception: + raise ValueError("Invalid BIP32 path", nstr) From 28dfc2cf60941f1a5b1878e1442c2fe7bc2f8807 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 14:28:17 -0400 Subject: [PATCH 187/634] Use KeyOriginInfo in hd_keypaths --- hwilib/base58.py | 6 ++---- hwilib/devices/bitbox02.py | 23 +++++++++++++---------- hwilib/devices/coldcard.py | 2 +- hwilib/devices/digitalbitbox.py | 10 ++-------- hwilib/devices/ledger.py | 9 +++------ hwilib/devices/trezor.py | 14 +++++++------- hwilib/key.py | 2 +- hwilib/serializations.py | 18 +++++++++--------- test/test_device.py | 5 +++-- 9 files changed, 41 insertions(+), 48 deletions(-) diff --git a/hwilib/base58.py b/hwilib/base58.py index 03d384bf9..efc8d0346 100644 --- a/hwilib/base58.py +++ b/hwilib/base58.py @@ -9,7 +9,6 @@ # import hashlib -import struct from binascii import hexlify, unhexlify from typing import List b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' @@ -72,11 +71,10 @@ def decode(s: str) -> bytes: break return b'\x00' * pad + res -def get_xpub_fingerprint(s: str) -> int: +def get_xpub_fingerprint(s: str) -> bytes: data = decode(s) fingerprint = data[5:9] - result: int = struct.unpack(" str: data = decode(xpub) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 04e7036f9..bda9e07f3 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -10,8 +10,6 @@ Sequence, TypeVar, ) -from binascii import unhexlify -import struct import builtins import sys from functools import wraps @@ -36,6 +34,9 @@ handle_errors, common_err_msgs, ) +from ..key import ( + KeyOriginInfo, +) import hid # type: ignore @@ -299,13 +300,16 @@ def init(self, expect_initialized: bool = True) -> bitbox02.BitBox02: def close(self) -> None: self.transport.close() - def get_master_fingerprint_hex(self) -> str: + def get_master_fingerprint(self) -> bytes: """ HWI by default retrieves the fingerprint at m/ by getting the xpub at m/0', which contains the parent fingerprint. The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/. """ bb02 = self.init() - return bb02.root_fingerprint().hex() + return bb02.root_fingerprint() + + def get_master_fingerprint_hex(self) -> str: + return self.get_master_fingerprint().hex() def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: raise UnavailableActionError( @@ -375,23 +379,22 @@ def display_address( @bitbox02_exception def sign_tx(self, psbt: PSBT) -> Dict[str, str]: def find_our_key( - keypaths: Dict[bytes, Sequence[int]] + keypaths: Dict[bytes, KeyOriginInfo] ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: """ Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey. Returns the pubkey and the keypath, without the fingerprint. """ - for pubkey, keypath_with_fingerprint in keypaths.items(): - fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:] + for pubkey, origin in keypaths.items(): # Cheap check if the key is ours. - if fp != master_fp: + if origin.fingerprint != master_fp: continue # Expensive check if the key is ours. # TODO: check for fingerprint collision # keypath_account = keypath[:-2] - return pubkey, keypath + return pubkey, origin.path return None, None def get_simple_type( @@ -409,7 +412,7 @@ def get_simple_type( "Input script type not recognized of input {}.".format(input_index) ) - master_fp = struct.unpack(" passes: passes = our_keys diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index d9f694854..435b0cf09 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -458,15 +458,9 @@ def sign_tx(self, tx): # Figure out which keypath thing is for this input for pubkey, keypath in psbt_in.hd_keypaths.items(): - if master_fp == keypath[0]: + if master_fp == keypath.fingerprint: # Add the keypath strings - keypath_str = 'm' - for index in keypath[1:]: - keypath_str += '/' - if index >= 0x80000000: - keypath_str += str(index - 0x80000000) + 'h' - else: - keypath_str += str(index) + keypath_str = keypath.get_derivation_path() # Create tuples and add to List tup = (binascii.hexlify(sighash).decode(), keypath_str, i_num, pubkey) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index de0cf0951..32bda0092 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -204,7 +204,7 @@ def sign_tx(self, tx): # Wallets shouldn't be sending to change address as user action # otherwise this will get confused for pubkey, path in tx.outputs[i_num].hd_keypaths.items(): - if struct.pack(" 2 and path[-2] == 1: + if path.fingerprint == master_fpr and len(path.path) > 1 and path[-1] == 1: # For possible matches, check if pubkey matches possible template if hash160(pubkey) in txout.scriptPubKey or hash160(bytearray.fromhex("0014") + hash160(pubkey)) in txout.scriptPubKey: change_path = '' @@ -277,12 +277,9 @@ def sign_tx(self, tx): # Figure out which keys in inputs are from our wallet for pubkey in pubkeys: keypath = psbt_in.hd_keypaths[pubkey] - if master_fpr == struct.pack(" None: self.path: Sequence[int] = path @classmethod - def deserialize(cls, s: bytes) -> object: + def deserialize(cls, s: bytes) -> 'KeyOriginInfo': """ Deserialize a serialized KeyOriginInfo. They will be serialized in the same way that PSBTs serialize derivation paths diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 773aa4411..4fb79ca1c 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -15,14 +15,16 @@ ser_*, deser_*: functions that handle serialization/deserialization """ -from io import BytesIO, BufferedReader from .errors import PSBTSerializationError +from .key import KeyOriginInfo import struct import binascii import hashlib import copy import base64 + +from io import BytesIO, BufferedReader from typing import ( Dict, List, @@ -34,7 +36,6 @@ TypeVar, Callable, ) - from typing_extensions import Protocol class Readable(Protocol): @@ -493,7 +494,7 @@ def __repr__(self) -> str: def DeserializeHDKeypath( f: Readable, key: bytes, - hd_keypaths: MutableMapping[bytes, Sequence[int]], + hd_keypaths: MutableMapping[bytes, KeyOriginInfo], ) -> None: if len(key) != 34 and len(key) != 66: raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") @@ -501,14 +502,13 @@ def DeserializeHDKeypath( if pubkey in hd_keypaths: raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") - value = deser_string(f) - hd_keypaths[pubkey] = list(struct.unpack("<" + "I" * (len(value) // 4), value)) + hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) -def SerializeHDKeypath(hd_keypaths: Mapping[bytes, Sequence[int]], type: bytes) -> bytes: +def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) -> bytes: r = b"" for pubkey, path in sorted(hd_keypaths.items()): r += ser_string(type + pubkey) - packed = struct.pack("<" + "I" * len(path), *path) + packed = path.serialize() r += ser_string(packed) return r @@ -520,7 +520,7 @@ def __init__(self) -> None: self.sighash = 0 self.redeem_script = b"" self.witness_script = b"" - self.hd_keypaths: Dict[bytes, Sequence[int]] = {} + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} self.final_script_sig = b"" self.final_script_witness = CTxInWitness() self.unknown: Dict[bytes, bytes] = {} @@ -680,7 +680,7 @@ class PartiallySignedOutput: def __init__(self) -> None: self.redeem_script = b"" self.witness_script = b"" - self.hd_keypaths: Dict[bytes, Sequence[int]] = {} + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} self.unknown: Dict[bytes, bytes] = {} def set_null(self) -> None: diff --git a/test/test_device.py b/test/test_device.py index 25e40fe48..058ea55de 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -14,6 +14,7 @@ from hwilib.base58 import xpub_to_pub_hex from hwilib.cli import process_commands from hwilib.descriptor import AddChecksum +from hwilib.key import KeyOriginInfo from hwilib.serializations import PSBT SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} @@ -289,9 +290,9 @@ def _generate_and_finalize(self, unknown_inputs, psbt): # Single input PSBTs will be fully signed by first signer for psbt_input in first_psbt.inputs[1:]: for pubkey, path in psbt_input.hd_keypaths.items(): - psbt_input.hd_keypaths[pubkey] = [0] + path[1:] + psbt_input.hd_keypaths[pubkey] = KeyOriginInfo(b"\x00\x00\x00\x00", path.path) for pubkey, path in second_psbt.inputs[0].hd_keypaths.items(): - second_psbt.inputs[0].hd_keypaths[pubkey] = [0] + path[1:] + second_psbt.inputs[0].hd_keypaths[pubkey] = KeyOriginInfo(b"\x00\x00\x00\x00", path.path) single_input = len(first_psbt.inputs) == 1 From d9291af4c30635bec884605222448b68218cb9a5 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 14:28:40 -0400 Subject: [PATCH 188/634] Use parse_path instead of trezorlib.tools.parse_path --- hwilib/devices/bitbox02.py | 39 +++----------------------------------- hwilib/devices/trezor.py | 10 +++++----- 2 files changed, 8 insertions(+), 41 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index bda9e07f3..fb93d2fb2 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -36,6 +36,7 @@ ) from ..key import ( KeyOriginInfo, + parse_path, ) import hid # type: ignore @@ -134,40 +135,6 @@ def attestation_check(self, result: bool) -> None: ) -def _parse_path(nstr: str) -> Sequence[int]: - """ - Adapted from trezorlib.tools.parse_path. - Convert BIP32 path string to list of uint32 integers with hardened flags. - Several conventions are supported to set the hardened flag: -1, 1', 1h - - e.g.: "0/1h/1" -> [0, 0x80000001, 1] - - :param nstr: path string - :return: list of integers - """ - if not nstr: - return [] - - n = nstr.split("/") - - # m/a/b/c => a/b/c - if n[0] == "m": - n = n[1:] - - def str_to_harden(x: str) -> int: - if x.startswith("-"): - return abs(int(x)) + HARDENED - elif x.endswith(("h", "'")): - return int(x[:-1]) + HARDENED - else: - return int(x) - - try: - return [str_to_harden(x) for x in n] - except Exception: - raise ValueError("Invalid BIP32 path", nstr) - - def enumerate(password: str = "") -> List[Dict[str, object]]: """ Enumerate all BitBox02 devices. Bootloaders excluded. @@ -337,7 +304,7 @@ def _get_xpub(self, keypath: Sequence[int]) -> str: ) def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: - path_uint32s = _parse_path(bip32_path) + path_uint32s = parse_path(bip32_path) try: xpub = self._get_xpub(path_uint32s) except Bitbox02Exception as exc: @@ -369,7 +336,7 @@ def display_address( "The BitBox02 does not support legacy p2pkh addresses" ) address = self.init().btc_address( - _parse_path(bip32_path), + parse_path(bip32_path), coin=self._get_coin(), script_config=script_config, display=True, diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index b7fef8e64..3aeb489af 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -34,7 +34,6 @@ prompt, ) from .trezorlib import ( - tools, btc, device, ) @@ -47,6 +46,7 @@ from ..key import ( ExtendedKey, + parse_path, ) from ..serializations import ( CTxOut, @@ -167,7 +167,7 @@ def _check_unlocked(self): def get_pubkey_at_path(self, path): self._check_unlocked() try: - expanded_path = tools.parse_path(path) + expanded_path = parse_path(path) except ValueError as e: raise BadArgumentError(str(e)) output = btc.get_public_node(self.client, expanded_path, coin_name=self.coin_name) @@ -407,7 +407,7 @@ def ignore_input(): @trezor_exception def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: self._check_unlocked() - path = tools.parse_path(keypath) + path = parse_path(keypath) result = btc.sign_message(self.client, self.coin_name, path, message) return {'signature': base64.b64encode(result.signature).decode('utf-8')} @@ -423,7 +423,7 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc for i in range(0, descriptor.multisig_N): xpub.deserialize(descriptor.base_key[i]) hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) - pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=tools.parse_path('m' + descriptor.path_suffix[i]))) + pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=parse_path('m' + descriptor.path_suffix[i]))) multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys) # redeem_script means p2sh/multisig elif redeem_script: @@ -451,7 +451,7 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc for path in keypath.split(','): if len(path.split('/')[0]) == 8: path = path.split('/', 1)[1] - expanded_path = tools.parse_path(path) + expanded_path = parse_path(path) try: address = btc.get_address( From eeadbd6a680601cd7ca3d1e5519012fc36049aef Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 19:24:45 -0400 Subject: [PATCH 189/634] Use less restrictive dependency version specifications Let ecdsa, mnemonic, and hidapi to update newer versions Regenerates the setup.py --- pyproject.toml | 6 +++--- setup.py | 31 +++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 55e0f70c1..29dd1a9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,10 +17,10 @@ packages = [ [tool.poetry.dependencies] python = "^3.6,<3.9" -hidapi = "^0.7.99" -ecdsa = "^0.13.0" +hidapi = "~0" +ecdsa = "~0" pyaes = "^1.6" -mnemonic = "^0.18.0" +mnemonic = "~0" typing-extensions = "^3.7" libusb1 = "^1.7" pyside2 = { version = "^5.14.0", optional = true } diff --git a/setup.py b/setup.py index 0d2116381..cd10f7806 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,16 @@ package_data = \ {'': ['*'], 'hwilib': ['udev/*', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', + 'ui/bitbox02pairing.ui', 'ui/displayaddressdialog.ui', 'ui/displayaddressdialog.ui', 'ui/displayaddressdialog.ui', @@ -22,6 +32,8 @@ 'ui/displayaddressdialog.ui', 'ui/displayaddressdialog.ui', 'ui/displayaddressdialog.ui', + 'ui/displayaddressdialog.ui', + 'ui/getkeypooloptionsdialog.ui', 'ui/getkeypooloptionsdialog.ui', 'ui/getkeypooloptionsdialog.ui', 'ui/getkeypooloptionsdialog.ui', @@ -40,6 +52,7 @@ 'ui/getxpubdialog.ui', 'ui/getxpubdialog.ui', 'ui/getxpubdialog.ui', + 'ui/getxpubdialog.ui', 'ui/hwiqt.pyproject', 'ui/hwiqt.pyproject', 'ui/hwiqt.pyproject', @@ -49,6 +62,8 @@ 'ui/hwiqt.pyproject', 'ui/hwiqt.pyproject', 'ui/hwiqt.pyproject', + 'ui/hwiqt.pyproject', + 'ui/mainwindow.ui', 'ui/mainwindow.ui', 'ui/mainwindow.ui', 'ui/mainwindow.ui', @@ -67,6 +82,8 @@ 'ui/sendpindialog.ui', 'ui/sendpindialog.ui', 'ui/sendpindialog.ui', + 'ui/sendpindialog.ui', + 'ui/setpassphrasedialog.ui', 'ui/setpassphrasedialog.ui', 'ui/setpassphrasedialog.ui', 'ui/setpassphrasedialog.ui', @@ -85,6 +102,8 @@ 'ui/signmessagedialog.ui', 'ui/signmessagedialog.ui', 'ui/signmessagedialog.ui', + 'ui/signmessagedialog.ui', + 'ui/signpsbtdialog.ui', 'ui/signpsbtdialog.ui', 'ui/signpsbtdialog.ui', 'ui/signpsbtdialog.ui', @@ -98,13 +117,13 @@ modules = \ ['hwi', 'hwi-qt'] install_requires = \ -['ecdsa>=0.13.0,<0.14.0', - 'hidapi>=0.7.99,<0.8.0', +['bitbox02>=4.1.0', + 'ecdsa>=0,<1', + 'hidapi>=0,<1', 'libusb1>=1.7,<2.0', - 'mnemonic>=0.18.0,<0.19.0', + 'mnemonic>=0,<1', 'pyaes>=1.6,<2.0', - 'typing-extensions>=3.7,<4.0', - 'bitbox02>=4.1.0'] + 'typing-extensions>=3.7,<4.0'] extras_require = \ {'qt': ['pyside2>=5.14.0,<6.0.0']} @@ -116,7 +135,7 @@ 'name': 'hwi', 'version': '1.1.2', 'description': 'A library for working with Bitcoin hardware wallets', - 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | N/A | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", + 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', 'author_email': 'andrew@achow101.com', 'maintainer': None, From acbfd3d35de198e657cb9d6ce165ec0bb0a4a702 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 19:26:10 -0400 Subject: [PATCH 190/634] Update poetry.lock --- poetry.lock | 274 +++++++++++++++++++++++++++------------------------- 1 file changed, 141 insertions(+), 133 deletions(-) diff --git a/poetry.lock b/poetry.lock index 232ed0eed..c614e6d45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,10 +12,11 @@ description = "A tool that automatically formats Python code to conform to the P name = "autopep8" optional = false python-versions = "*" -version = "1.5.2" +version = "1.5.4" [package.dependencies] -pycodestyle = ">=2.5.0" +pycodestyle = ">=2.6.0" +toml = "*" [[package]] category = "main" @@ -48,7 +49,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = false python-versions = "*" -version = "1.14.1" +version = "1.14.2" [package.dependencies] pycparser = "*" @@ -59,7 +60,7 @@ description = "cryptography is a package which provides cryptographic recipes an name = "cryptography" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "3.0" +version = "3.1" [package.dependencies] cffi = ">=1.8,<1.11.3 || >1.11.3" @@ -68,7 +69,6 @@ six = ">=1.4.1" [package.extras] docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -idna = ["idna (>=2.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] @@ -86,8 +86,15 @@ category = "main" description = "ECDSA cryptographic signature library (pure python)" name = "ecdsa" optional = false -python-versions = "*" -version = "0.13.3" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.16.0" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] [[package]] category = "dev" @@ -95,7 +102,7 @@ description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.1" +version = "3.8.3" [package.dependencies] mccabe = ">=0.6.0,<0.7.0" @@ -117,11 +124,11 @@ version = "0.18.2" [[package]] category = "main" -description = "A Cython interface to the hidapi from https://github.com/signal11/hidapi" +description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" name = "hidapi" optional = false python-versions = "*" -version = "0.7.99.post21" +version = "0.9.0.post3" [package.dependencies] setuptools = ">=19.0" @@ -133,14 +140,14 @@ marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.6.0" +version = "1.7.0" [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["sphinx", "rst.linker"] -testing = ["packaging", "importlib-resources"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] category = "main" @@ -176,10 +183,7 @@ description = "Implementation of Bitcoin BIP-0039" name = "mnemonic" optional = false python-versions = "*" -version = "0.18" - -[package.dependencies] -pbkdf2 = "*" +version = "0.19" [[package]] category = "main" @@ -192,14 +196,6 @@ version = "0.3.1" [package.dependencies] cryptography = ">=2.8" -[[package]] -category = "main" -description = "PKCS#5 v2.0 PBKDF2 Module" -name = "pbkdf2" -optional = false -python-versions = "*" -version = "1.3" - [[package]] category = "dev" description = "Python PE parsing module" @@ -218,7 +214,7 @@ description = "Protocol Buffers" name = "protobuf" optional = false python-versions = "*" -version = "3.12.4" +version = "3.13.0" [package.dependencies] setuptools = "*" @@ -275,10 +271,10 @@ description = "Python bindings for the Qt cross-platform application and UI fram name = "pyside2" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.14.2.1" +version = "5.15.0" [package.dependencies] -shiboken2 = "5.14.2.1" +shiboken2 = "5.15.0" [[package]] category = "dev" @@ -303,7 +299,7 @@ description = "Python / C++ bindings helper module" name = "shiboken2" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.14.2.1" +version = "5.15.0" [[package]] category = "main" @@ -313,13 +309,21 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" version = "1.15.0" +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + [[package]] category = "main" description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.2" +version = "3.7.4.3" [[package]] category = "dev" @@ -338,8 +342,7 @@ testing = ["jaraco.itertools", "func-timeout"] qt = ["pyside2"] [metadata] -content-hash = "fc39b2be42f870113feb38f56c177164fb7302eb07616d850f5a1b9e3e8509e1" -lock-version = "1.0" +content-hash = "65161c3ade35941a049983c5a9ac4f7ec8b8ce040fb4999328e871b5c7430daf" python-versions = "^3.6,<3.9" [metadata.files] @@ -348,7 +351,7 @@ altgraph = [ {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, ] autopep8 = [ - {file = "autopep8-1.5.2.tar.gz", hash = "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"}, + {file = "autopep8-1.5.4.tar.gz", hash = "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"}, ] base58 = [ {file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"}, @@ -359,55 +362,58 @@ bitbox02 = [ {file = "bitbox02-4.1.0.tar.gz", hash = "sha256:73a35594162f32897dd2b1880f0cfaa42922acd1c2d7f4cf3d94b8333329c931"}, ] cffi = [ - {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, - {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, - {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, - {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, - {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, - {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, - {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, - {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, - {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, - {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, - {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, - {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, - {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, - {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, - {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, - {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, + {file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"}, + {file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"}, + {file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"}, + {file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"}, + {file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"}, + {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"}, + {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"}, + {file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"}, + {file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"}, + {file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"}, + {file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"}, + {file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"}, + {file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"}, + {file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"}, + {file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"}, + {file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"}, + {file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"}, + {file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"}, + {file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"}, + {file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"}, + {file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"}, + {file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"}, + {file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"}, + {file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"}, + {file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"}, + {file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"}, + {file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"}, + {file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"}, ] cryptography = [ - {file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"}, - {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"}, - {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"}, - {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"}, - {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"}, - {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"}, - {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"}, - {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"}, - {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"}, - {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"}, - {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"}, - {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"}, - {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"}, - {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"}, - {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"}, - {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"}, - {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"}, + {file = "cryptography-3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36"}, + {file = "cryptography-3.1-cp27-cp27m-win32.whl", hash = "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a"}, + {file = "cryptography-3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e"}, + {file = "cryptography-3.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8"}, + {file = "cryptography-3.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237"}, + {file = "cryptography-3.1-cp35-cp35m-win32.whl", hash = "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716"}, + {file = "cryptography-3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695"}, + {file = "cryptography-3.1-cp36-abi3-win32.whl", hash = "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af"}, + {file = "cryptography-3.1-cp36-abi3-win_amd64.whl", hash = "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618"}, + {file = "cryptography-3.1-cp36-cp36m-win32.whl", hash = "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1"}, + {file = "cryptography-3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c"}, + {file = "cryptography-3.1-cp37-cp37m-win32.whl", hash = "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32"}, + {file = "cryptography-3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed"}, + {file = "cryptography-3.1-cp38-cp38-win32.whl", hash = "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"}, + {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, + {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, ] dis3 = [ {file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"}, @@ -415,32 +421,30 @@ dis3 = [ {file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"}, ] ecdsa = [ - {file = "ecdsa-0.13.3-py2.py3-none-any.whl", hash = "sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db"}, - {file = "ecdsa-0.13.3.tar.gz", hash = "sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d"}, + {file = "ecdsa-0.16.0-py2.py3-none-any.whl", hash = "sha256:ca359c971594dceebf334f3d623dae43163ab161c7d09f28cae70a86df26eb7a"}, + {file = "ecdsa-0.16.0.tar.gz", hash = "sha256:494c6a853e9ed2e9be33d160b41d47afc50a6629b993d2b9c5ad7bb226add892"}, ] flake8 = [ - {file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"}, - {file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"}, + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, ] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] hidapi = [ - {file = "hidapi-0.7.99.post21-cp27-cp27m-win32.whl", hash = "sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660"}, - {file = "hidapi-0.7.99.post21-cp27-cp27m-win_amd64.whl", hash = "sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24"}, - {file = "hidapi-0.7.99.post21-cp34-cp34m-win32.whl", hash = "sha256:d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa"}, - {file = "hidapi-0.7.99.post21-cp34-cp34m-win_amd64.whl", hash = "sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7"}, - {file = "hidapi-0.7.99.post21-cp35-cp35m-win32.whl", hash = "sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b"}, - {file = "hidapi-0.7.99.post21-cp35-cp35m-win_amd64.whl", hash = "sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87"}, - {file = "hidapi-0.7.99.post21-cp36-cp36m-win32.whl", hash = "sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922"}, - {file = "hidapi-0.7.99.post21-cp36-cp36m-win_amd64.whl", hash = "sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946"}, - {file = "hidapi-0.7.99.post21-cp37-cp37m-win32.whl", hash = "sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7"}, - {file = "hidapi-0.7.99.post21-cp37-cp37m-win_amd64.whl", hash = "sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3"}, - {file = "hidapi-0.7.99.post21.tar.gz", hash = "sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97"}, + {file = "hidapi-0.9.0.post3-cp35-cp35m-win32.whl", hash = "sha256:98bada9a2625a90a452b17b237a342c29142677c77dd0ba96072f45b0e55d5ec"}, + {file = "hidapi-0.9.0.post3-cp35-cp35m-win_amd64.whl", hash = "sha256:82d6276337d7cc25acda8b5fa99e0db497090c369611eefa18ea69c9afe55ed7"}, + {file = "hidapi-0.9.0.post3-cp36-cp36m-win32.whl", hash = "sha256:92995887078d8e7b768a60b597d1117b1aba0a5184538b633be7192daeba34cc"}, + {file = "hidapi-0.9.0.post3-cp36-cp36m-win_amd64.whl", hash = "sha256:4ee5bf9f2ece8ac73ef01f0a56ea6f62dcf024ba3beba6b29d3d52d96112931e"}, + {file = "hidapi-0.9.0.post3-cp37-cp37m-win32.whl", hash = "sha256:12288a950d7c7c3756f25405b74eb17ad84032c06a65bbbe78adea8dd247f4c0"}, + {file = "hidapi-0.9.0.post3-cp37-cp37m-win_amd64.whl", hash = "sha256:3910117ee13f3730f6810cf4b591f84dc4b55258163cbbdf9135b55deced1775"}, + {file = "hidapi-0.9.0.post3-cp38-cp38-win32.whl", hash = "sha256:a1bf3893353f654613fecc10259097d417e76ff8799f3be459aed7d1e9cee7fd"}, + {file = "hidapi-0.9.0.post3-cp38-cp38-win_amd64.whl", hash = "sha256:f70e0609c36605d3c06a91fbccc058e255918af2c59872648fe551360ad68df5"}, + {file = "hidapi-0.9.0.post3.tar.gz", hash = "sha256:5a2442928f17ba742d9c53073f48b152051c5747d758d2fefd937543da5ab2e5"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, - {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, ] libusb1 = [ {file = "libusb1-1.8.tar.gz", hash = "sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6"}, @@ -454,36 +458,34 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mnemonic = [ - {file = "mnemonic-0.18.tar.gz", hash = "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"}, + {file = "mnemonic-0.19-py2.py3-none-any.whl", hash = "sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6"}, + {file = "mnemonic-0.19.tar.gz", hash = "sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931"}, ] noiseprotocol = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, ] -pbkdf2 = [ - {file = "pbkdf2-1.3.tar.gz", hash = "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"}, -] pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, ] protobuf = [ - {file = "protobuf-3.12.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3d59825cba9447e8f4fcacc1f3c892cafd28b964e152629b3f420a2fb5918b5a"}, - {file = "protobuf-3.12.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6009f3ebe761fad319b52199a49f1efa7a3729302947a78a3f5ea8e7e89e3ac2"}, - {file = "protobuf-3.12.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e2bd5c98952db3f1bb1af2e81b6a208909d3b8a2d32f7525c5cc10a6338b6593"}, - {file = "protobuf-3.12.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2becd0e238ae34caf96fa7365b87f65b88aebcf7864dfe5ab461c5005f4256d9"}, - {file = "protobuf-3.12.4-cp35-cp35m-win32.whl", hash = "sha256:ef991cbe34d7bb935ba6349406a210d3558b9379c21621c6ed7b99112af7350e"}, - {file = "protobuf-3.12.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a7b6cf201e67132ca99b8a6c4812fab541fdce1ceb54bb6f66bc336ab7259138"}, - {file = "protobuf-3.12.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4794a7748ee645d2ae305f3f4f0abd459e789c973b5bc338008960f83e0c554b"}, - {file = "protobuf-3.12.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f1796e0eb911bf5b08e76b753953effbeb6bc42c95c16597177f627eaa52c375"}, - {file = "protobuf-3.12.4-cp36-cp36m-win32.whl", hash = "sha256:c0c8d7c8f07eacd9e98a907941b56e57883cf83de069cfaeaa7e02c582f72ddb"}, - {file = "protobuf-3.12.4-cp36-cp36m-win_amd64.whl", hash = "sha256:2db6940c1914fa3fbfabc0e7c8193d9e18b01dbb4650acac249b113be3ba8d9e"}, - {file = "protobuf-3.12.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6842284bb15f1b19c50c5fd496f1e2a4cfefdbdfa5d25c02620cb82793295a7"}, - {file = "protobuf-3.12.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0b00429b87821f1e6f3d641327864e6f271763ae61799f7540bc58a352825fe2"}, - {file = "protobuf-3.12.4-cp37-cp37m-win32.whl", hash = "sha256:f10ba89f9cd508dc00e469918552925ef7cba38d101ca47af1e78f2f9982c6b3"}, - {file = "protobuf-3.12.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2636c689a6a2441da9a2ef922a21f9b8bfd5dfe676abd77d788db4b36ea86bee"}, - {file = "protobuf-3.12.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50b7bb2124f6a1fb0ddc6a44428ae3a21e619ad2cdf08130ac6c00534998ef07"}, - {file = "protobuf-3.12.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e77ca4e1403b363a88bde9e31c11d093565e925e1685f40b29385a52f2320794"}, - {file = "protobuf-3.12.4-py2.py3-none-any.whl", hash = "sha256:32f0bcdf85e0040f36b4f548c71177027f2a618cab00ba235197fa9e230b7289"}, - {file = "protobuf-3.12.4.tar.gz", hash = "sha256:c99e5aea75b6f2b29c8d8da5bdc5f5ed8d9a5b4f15115c8316a3f0a850f94656"}, + {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, + {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, + {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, + {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, + {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, + {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, + {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, + {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, + {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, + {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, + {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, + {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, + {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, + {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, + {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, + {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, + {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, + {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, ] pyaes = [ {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, @@ -504,12 +506,12 @@ pyinstaller = [ {file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"}, ] pyside2 = [ - {file = "PySide2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:71d0416e69f0ac4d5d0f9892c819b2896a4e821bc83b29932769060119f3292c"}, - {file = "PySide2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d5054009ced176cf78d71d2787bdcd676bc990004bec0e7079f7bdfe7edffe5"}, - {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:a6390619b9c8713ba190dd3fa116a2d9f63597cb0f902fbf31b2657023936a3a"}, - {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:174e1863ae3526bae8ae946b24cccb472dffd7e643bc47ae4d2de39cac583a9c"}, - {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:e21058fcd8d2cb9871fc61f9d35ed15f0e0c4718c5d463a2e37be1d67b8c40b4"}, - {file = "PySide2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:93c19beef80ef54392e6cd0de937be4e1603560229eb38738c8b50bbb8da90f7"}, + {file = "PySide2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:ae8158d611a410c58091aa8baf24005894b4e3f40c63ff2482149481ad5395b4"}, + {file = "PySide2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:de0220cc01a8bfdaa8ccd0fc934a1ead2aedca62b49b5fd4bdcdaba6f4585a03"}, + {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:1478ea8a1ab5d8bc021ce41211933fbc238338fe70c02f7bcc2e80ea900dbf9e"}, + {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:f9099e49fb2d3571f5a81eb9ff281ce832ce8c333052e8175e2356b9c3e4a882"}, + {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:7c91a5074f3c60bac7e9336943a1dc9d5c8be8ab88a232dc55018e555dae81b2"}, + {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:2d72150f63025b9b55097c1a64d09da37ff9191f73f69237500dec7a4a130541"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, @@ -520,21 +522,27 @@ semver = [ {file = "semver-2.10.2.tar.gz", hash = "sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276"}, ] shiboken2 = [ - {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d285d476a76f254bff69cc58c1d4385df295b42de1a818d4a8d11694c2d728fc"}, - {file = "shiboken2-5.14.2.1-5.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73d03e74f542204e351539e42ab3e3727a69408e1497af4c6e84fb66c3e706d8"}, - {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:16b59490228bf923ea7c8ed6edcb4f7349ce5a5fc30369190c41487baf6d4aaa"}, - {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:cd6ba0ba0d070c8ec090ad3eb10440989f7e5a4404c6b087f8f695a75a01e1dc"}, - {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:fe4d0cf6737f1d01944be4cf3b401d74015c515ab84622bf04f47d64ffcd39f9"}, - {file = "shiboken2-5.14.2.1-5.14.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:c022203b7cf01df6ad0bb190d286c2965958243a16e47bee8c5e6bbb9d0cd475"}, + {file = "shiboken2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:0826ce788fe55bce19a8f8a2c33d720a6ba8f59e1aab1fa9d7a53eceed3f3af5"}, + {file = "shiboken2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a92c55363d5cd3cfdd6cd28dcf91e81a00a3aa5bb177d712817c09d26bd760db"}, + {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:41a9157fb9cc7e0c0747926b25c23c3f94d59d61736a6ff763ebc7acf6afc5cf"}, + {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:5702e77ad5999ac45498c3cd47f5d078ce7406cf8dc8df74337b0cdc084bf762"}, + {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:4b0904e0967356a36e80cde05981faa14c120141856d973ee983eac0b83633c0"}, + {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:19d5f715e5ae8a815a7f148a8614a3225dceee6fd9d5decaa7749657f0f7ccbe"}, + {file = "shiboken2-5.15.0-5.15.0_1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:94991848e9ff4d03c2d7feab484113b5b5ad7f9fdfa0b0ff46ce18da47b36b58"}, + {file = "shiboken2-5.15.0-5.15.0_2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:e753324a78cbdab1c5917b5600c708a8db7e1336579e7afa20ed90edda15eefa"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] zipp = [ {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, From 3224b2c3aadaf37518a5c89060c6b250fbfe186e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 19:26:50 -0400 Subject: [PATCH 191/634] Update pyinstaller to 4.0 --- poetry.lock | 42 ++++++++++++++++++++++++------------------ pyproject.toml | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index c614e6d45..44982aa94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -73,14 +73,6 @@ pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] -[[package]] -category = "dev" -description = "Python 2.7 backport of the \"dis\" module from Python 3.5+" -name = "dis3" -optional = false -python-versions = "*" -version = "0.1.3" - [[package]] category = "main" description = "ECDSA cryptographic signature library (pure python)" @@ -257,14 +249,29 @@ category = "dev" description = "PyInstaller bundles a Python application and all its dependencies into a single package." name = "pyinstaller" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "3.6" +python-versions = "*" +version = "4.0" [package.dependencies] altgraph = "*" -dis3 = "*" +macholib = ">=1.8" +pefile = ">=2017.8.1" +pyinstaller-hooks-contrib = ">=2020.6" +pywin32-ctypes = ">=0.2.0" setuptools = "*" +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] + +[[package]] +category = "dev" +description = "Community maintained hooks for PyInstaller" +name = "pyinstaller-hooks-contrib" +optional = false +python-versions = "*" +version = "2020.7" + [[package]] category = "main" description = "Python bindings for the Qt cross-platform application and UI framework" @@ -342,7 +349,7 @@ testing = ["jaraco.itertools", "func-timeout"] qt = ["pyside2"] [metadata] -content-hash = "65161c3ade35941a049983c5a9ac4f7ec8b8ce040fb4999328e871b5c7430daf" +content-hash = "6bee57172b9beaa2587a228e24a3c7588d2472f228b4bd845ff0cc8c99f4374f" python-versions = "^3.6,<3.9" [metadata.files] @@ -415,11 +422,6 @@ cryptography = [ {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, ] -dis3 = [ - {file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"}, - {file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"}, - {file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"}, -] ecdsa = [ {file = "ecdsa-0.16.0-py2.py3-none-any.whl", hash = "sha256:ca359c971594dceebf334f3d623dae43163ab161c7d09f28cae70a86df26eb7a"}, {file = "ecdsa-0.16.0.tar.gz", hash = "sha256:494c6a853e9ed2e9be33d160b41d47afc50a6629b993d2b9c5ad7bb226add892"}, @@ -503,7 +505,11 @@ pyflakes = [ {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pyinstaller = [ - {file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"}, + {file = "pyinstaller-4.0.tar.gz", hash = "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0"}, +] +pyinstaller-hooks-contrib = [ + {file = "pyinstaller-hooks-contrib-2020.7.tar.gz", hash = "sha256:74936d044f319cd7a9dca322b46a818fcb6e2af1c67af62e8a6a3121eb2863d2"}, + {file = "pyinstaller_hooks_contrib-2020.7-py2.py3-none-any.whl", hash = "sha256:5b6e06ba6072499189f5b8e1623d5f0414962941aac370ee4f842de25455be5b"}, ] pyside2 = [ {file = "PySide2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:ae8158d611a410c58091aa8baf24005894b4e3f40c63ff2482149481ad5395b4"}, diff --git a/pyproject.toml b/pyproject.toml index 29dd1a9f5..7e8db36a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ bitbox02 = ">=4.1.0" qt = ["pyside2"] [tool.poetry.dev-dependencies] -pyinstaller = "^3.4" +pyinstaller = "^4.0" pywin32-ctypes = {version = "^0.2.0",platform = "win32"} pefile = {version = "^2019.4",platform = "win32"} macholib = {version = "^1.11",platform = "darwin"} From c85879e505e060f38c8e9568717aa253c666eae2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 9 Sep 2020 12:28:18 -0400 Subject: [PATCH 192/634] Move str_to_int_path to coldcard ckcc-protocol moves this to the cli tool, so we just copy it here before we update ckcc. --- hwilib/devices/coldcard.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 8ef0de799..07a4cb5c5 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -32,9 +32,6 @@ AF_P2SH, AF_P2WSH_P2SH, ) -from .ckcc.utils import ( - str_to_int_path, -) from ..base58 import ( get_xpub_fingerprint, xpub_main_2_test, @@ -56,6 +53,30 @@ CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock' # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md + +def str_to_int_path(xfp, path): + # convert text m/34'/33/44 into BIP174 binary compat format + # - include hex for fingerprint (m) as first arg + + rv = [struct.unpack('= 2, i + here = int(i[:-1]) | 0x80000000 + else: + here = int(i) + assert 0 <= here < 0x80000000, here + + rv.append(here) + + return rv + + def coldcard_exception(f): def func(*args, **kwargs): try: From 9e633fa5c477ff1b428a6a28048c47cb00bc525c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 8 Sep 2020 12:39:39 -0400 Subject: [PATCH 193/634] Update ckcc vendor library Update to ckcc-protocol@ca8d2b7808784a9f4927f3250bf52d2623a4e15b --- hwilib/devices/ckcc/README.md | 2 +- hwilib/devices/ckcc/__init__.py | 2 +- hwilib/devices/ckcc/client.py | 29 ++++++++--- hwilib/devices/ckcc/constants.py | 19 +++++++ hwilib/devices/ckcc/protocol.py | 89 +++++++++++++++++++++++++++++--- hwilib/devices/ckcc/utils.py | 40 +++++++------- 6 files changed, 145 insertions(+), 36 deletions(-) diff --git a/hwilib/devices/ckcc/README.md b/hwilib/devices/ckcc/README.md index bfab35b02..8db249b74 100644 --- a/hwilib/devices/ckcc/README.md +++ b/hwilib/devices/ckcc/README.md @@ -2,7 +2,7 @@ This is a stripped down and modified version of the official [ckcc-protocol](https://github.com/Coldcard/ckcc-protocol) library. -This stripped down version was made at commit [49fa0265df4c9d0d0d915ccd4dc41b06104d6738](https://github.com/Coldcard/ckcc-protocol/tree/49fa0265df4c9d0d0d915ccd4dc41b06104d6738). +This stripped down version was made at commit [ca8d2b7808784a9f4927f3250bf52d2623a4e15b](https://github.com/Coldcard/ckcc-protocol/tree/ca8d2b7808784a9f4927f3250bf52d2623a4e15b). ## Changes diff --git a/hwilib/devices/ckcc/__init__.py b/hwilib/devices/ckcc/__init__.py index c016e7664..b2f0b70ea 100644 --- a/hwilib/devices/ckcc/__init__.py +++ b/hwilib/devices/ckcc/__init__.py @@ -1,5 +1,5 @@ -__version__ = '0.7.2' +__version__ = '1.0.2' __all__ = [ "client", "protocol", "constants" ] diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index 665da3a7e..1b77a9a41 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -8,7 +8,7 @@ # # - ec_mult, ec_setup, aes_setup, mitm_verify # -import hid, sys, os, platform +import hid, sys, os from binascii import b2a_hex, a2b_hex from hashlib import sha256 from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN @@ -27,8 +27,6 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.is_simulator = False if not dev and sn and '/' in sn: - if platform.system() == 'Windows': - raise RuntimeError("Cannot connect to simulator. Is it running?") dev = UnixSimulatorPipe(sn) found = 'simulator' self.is_simulator = True @@ -92,12 +90,12 @@ def resync(self): # check the above all worked err = self.dev.error() - if err != '': + if err and ('not implemented yet' not in err) and (err != 'Success'): raise RuntimeError('hidapi: '+err) assert self.dev.get_serial_number_string() == self.serial - def send_recv(self, msg, expect_errors=False, verbose=0, timeout=1000, encrypt=True): + def send_recv(self, msg, expect_errors=False, verbose=0, timeout=3000, encrypt=True): # first byte of each 64-byte packet encodes length or packet-offset assert 4 <= len(msg) <= MAX_MSG_LEN, "msg length: %d" % len(msg) @@ -140,6 +138,10 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=1000, encrypt=T while 1: buf = self.dev.read(64, timeout_ms=(timeout or 0)) + if not buf and timeout: + # give it another try + buf = self.dev.read(64, timeout_ms=timeout) + assert buf, "timeout reading USB EP" # (trusting more than usual here) @@ -217,8 +219,8 @@ def aes_setup(self, session_key): # - count must start at zero, and increment in LSB for each block. import pyaes - self.encrypt_request = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).decrypt - self.decrypt_response = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).encrypt + self.encrypt_request = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).encrypt + self.decrypt_response = pyaes.AESModeOfOperationCTR(session_key, pyaes.Counter(0)).decrypt def start_encryption(self): # setup encryption on the link @@ -258,6 +260,7 @@ def mitm_verify(self, sig, expected_xpub): # If Pycoin is not available, do it using ecdsa from ecdsa import BadSignatureError, SECP256k1, VerifyingKey + # of the returned (pubkey, chaincode) tuple, chaincode is not used pubkey, _ = decode_xpub(expected_xpub) vk = VerifyingKey.from_string(get_pubkey_string(pubkey), curve=SECP256k1) try: @@ -325,6 +328,15 @@ def download_file(self, length, checksum, blksize=1024, file_number=1): return data + def hash_password(self, text_password): + # Turn text password into a key for use in HSM auth protocol + from hashlib import pbkdf2_hmac, sha256 + from .constants import PBKDF2_ITER_COUNT + + salt = sha256(b'pepper' + self.serial.encode('ascii')).digest() + + return pbkdf2_hmac('sha256', text_password, salt, PBKDF2_ITER_COUNT) + class UnixSimulatorPipe: # Use a UNIX pipe to the simulator instead of a real USB connection. @@ -335,7 +347,8 @@ def __init__(self, path): self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) try: self.pipe.connect(path) - except FileNotFoundError: + except Exception: + self.close() raise RuntimeError("Cannot connect to simulator. Is it running?") instance = 0 diff --git a/hwilib/devices/ckcc/constants.py b/hwilib/devices/ckcc/constants.py index 94998382f..ebbd93770 100644 --- a/hwilib/devices/ckcc/constants.py +++ b/hwilib/devices/ckcc/constants.py @@ -24,6 +24,24 @@ # Max length of text messages for signing MSG_SIGNING_MAX_LENGTH = const(240) +# Types of user auth we support +USER_AUTH_TOTP = const(1) # RFC6238 +USER_AUTH_HOTP = const(2) # RFC4226 +USER_AUTH_HMAC = const(3) # PBKDF2('hmac-sha256', secret, sha256(psbt), PBKDF2_ITER_COUNT) +USER_AUTH_SHOW_QR = const(0x80) # show secret on Coldcard screen (best for TOTP enroll) + +MAX_USERNAME_LEN = 16 +PBKDF2_ITER_COUNT = 2500 + +# Max depth for derived keys, in PSBT files, and USB commands +MAX_PATH_DEPTH = const(12) + +# Bitmask used in sign_transaction (stxn) command +STXN_FINALIZE = const(0x01) +STXN_VISUALIZE = const(0x02) +STXN_SIGNED = const(0x04) +STXN_FLAGS_MASK = const(0x07) + # Bit values for address types AFC_PUBKEY = const(0x01) # pay to hash of pubkey AFC_SEGWIT = const(0x02) # requires a witness to spend @@ -51,6 +69,7 @@ # BIP-174 aka PSBT defined values # PSBT_GLOBAL_UNSIGNED_TX = const(0) +PSBT_GLOBAL_XPUB = const(1) PSBT_IN_NON_WITNESS_UTXO = const(0) PSBT_IN_WITNESS_UTXO = const(1) diff --git a/hwilib/devices/ckcc/protocol.py b/hwilib/devices/ckcc/protocol.py index e52e58801..92431910e 100644 --- a/hwilib/devices/ckcc/protocol.py +++ b/hwilib/devices/ckcc/protocol.py @@ -11,6 +11,12 @@ class CCProtoError(RuntimeError): def __str__(self): return self.args[0] +class CCFramingError(CCProtoError): + # Typically framing errors are caused by multiple + # programs trying to talk to Coldcard at same time, + # and the encryption state gets confused. + pass + class CCUserRefused(RuntimeError): def __str__(self): return 'You refused permission to do the operation' @@ -42,6 +48,15 @@ def ping(msg): # returns whatever binary you give it return b'ping' + bytes(msg) + @staticmethod + def bip39_passphrase(pw): + return b'pass' + bytes(pw, 'utf8') + + @staticmethod + def get_passphrase_done(): + # poll completion of BIP39 encryption change (provides root xpub) + return b'pwok' + @staticmethod def check_mitm(): return b'mitm' @@ -72,10 +87,11 @@ def sha256(): return b'sha2' @staticmethod - def sign_transaction(length, file_sha, finalize=False): + def sign_transaction(length, file_sha, finalize=False, flags=0x0): # must have already uploaded binary, and give expected sha256 assert len(file_sha) == 32 - return pack('<4sII32s', b'stxn', length, int(finalize), file_sha) + flags |= (STXN_FINALIZE if finalize else 0x00) + return pack('<4sII32s', b'stxn', length, int(flags), file_sha) @staticmethod def sign_message(raw_msg, subpath='m', addr_fmt=AF_CLASSIC): @@ -98,6 +114,17 @@ def get_signed_txn(): # poll completion/results of transaction signing return b'stok' + @staticmethod + def multisig_enroll(length, file_sha): + # multisig details must already be uploaded as a text file, this starts approval process. + assert len(file_sha) == 32 + return pack('<4sI32s', b'enrl', length, file_sha) + + @staticmethod + def multisig_check(M, N, xfp_xor): + # do we have a wallet already that matches M+N and xor(*xfps)? + return pack('<4s3I', b'msck', M, N, xfp_xor) + @staticmethod def get_xpub(subpath='m'): # takes a string, like: m/44'/0'/23/23 @@ -105,8 +132,9 @@ def get_xpub(subpath='m'): @staticmethod def show_address(subpath, addr_fmt=AF_CLASSIC): - # takes a string, like: m/44'/0'/23/23 - # shows on screen, no feedback from user expected + # - takes a string, like: m/44'/0'/23/23 + # - shows on screen, no feedback from user expected + assert not (addr_fmt & AFC_SCRIPT) return pack('<4sI', b'show', addr_fmt) + subpath.encode('ascii') @staticmethod @@ -131,6 +159,11 @@ def show_p2sh_address(M, xfp_paths, witdeem_script, addr_fmt=AF_P2SH): return rv + @staticmethod + def block_chain(): + # ask what blockchain it's set for; expect "BTC" or "XTN" + return b'blkc' + @staticmethod def sim_keypress(key): # Simulator ONLY: pretend a key is pressed @@ -141,6 +174,48 @@ def bag_number(new_number=b''): # one time only: put into bag, or readback bag return b'bagi' + bytes(new_number) + @staticmethod + def hsm_start(length=0, file_sha=b''): + if length: + # New policy already be uploaded as a JSON file, get approval and start. + assert len(file_sha) == 32 + return pack('<4sI32s', b'hsms', length, file_sha) + else: + # Use policy on device already. Confirmation still required by local user. + return b'hsms' + + @staticmethod + def hsm_status(): + # get current status of HSM mode and/or policy defined already. Returns JSON + return b'hsts' + + @staticmethod + def create_user(username, auth_mode, secret=b''): + # create username, with pre-shared secret/password, or we generate. + # auth_model should be one of USER_AUTH_* + # for TOTP/HOTP, secret can be empty. Set bit 0x80 in auth_mode and QR will be used + assert 1 <= len(username) <= MAX_USERNAME_LEN + assert len(secret) in { 0, 10, 20, 32} + return pack('<4sBBB', b'nwur', auth_mode, len(username), len(secret)) + username + secret + + @staticmethod + def delete_user(username): + # remove a username and forget secret; cannot be used in HSM mode (only before) + assert 0 < len(username) <= MAX_USERNAME_LEN + return pack('<4sB', b'rmur', len(username)) + username + + @staticmethod + def user_auth(username, token, totp_time=0): + # HSM mode: try an authentication method for a username + assert 0 < len(username) <= 16 + assert 6 <= len(token) <= 32 + return pack('<4sIBB', b'user', totp_time, len(username), len(token)) + username + token + + @staticmethod + def get_storage_locker(): + # returns up to 414 bytes of user-defined sensitive data + return b'gslr' + class CCProtocolUnpacker: # Take a binary response, and turn it into a python object @@ -156,7 +231,7 @@ def decode(cls, msg): d = getattr(cls, sign, cls) if d is cls: - raise CCProtoError('Unknown response signature: ' + repr(sign)) + raise CCFramingError('Unknown response signature: ' + repr(sign)) return d(msg) @@ -170,9 +245,9 @@ def okay(msg): # low-level errors def fram(msg): - raise CCProtoError("Framing Error", str(msg[4:], 'utf8')) + raise CCFramingError("Framing Error", str(msg[4:], 'utf8')) def err_(msg): - raise CCProtoError("Remote Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:]) + raise CCProtoError("Coldcard Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:]) def refu(msg): # user didn't want to approve something diff --git a/hwilib/devices/ckcc/utils.py b/hwilib/devices/ckcc/utils.py index 630ea1a64..538929326 100644 --- a/hwilib/devices/ckcc/utils.py +++ b/hwilib/devices/ckcc/utils.py @@ -85,24 +85,26 @@ def get_pubkey_string(b): y = p - y return x.to_bytes(32, byteorder="big") + y.to_bytes(32, byteorder="big") -def str_to_int_path(xfp, path): - # convert text m/34'/33/44 into BIP174 binary compat format - # - include hex for fingerprint (m) as first arg - - rv = [struct.unpack('= 2, i - here = int(i[:-1]) | 0x80000000 - else: - here = int(i) - assert 0 <= here < 0x80000000, here - - rv.append(here) - - return rv + +def calc_local_pincode(psbt_sha, next_local_code): + # In HSM mode, you will need this function to generate + # the next 6-digit code for the local user. + # + # - next_local_code comes from the hsm_status response + # - psbt_sha is sha256() over the binary PSBT you will be submitting + # + from binascii import a2b_base64 + from hashlib import sha256 + import struct, hmac + + key = a2b_base64(next_local_code) + assert len(key) >= 15 + assert len(psbt_sha) == 32 + digest = hmac.new(key, psbt_sha, sha256).digest() + + num = struct.unpack('>I', digest[-4:])[0] & 0x7fffffff + + return '%06d' % (num % 1000000) + # EOF From 5b712ee12a867698c91fdbb8728314a75b6758ea Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 9 Sep 2020 15:21:32 -0400 Subject: [PATCH 194/634] HWI patches for ckcc --- hwilib/devices/ckcc/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index 1b77a9a41..bd4b91ea0 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -8,7 +8,7 @@ # # - ec_mult, ec_setup, aes_setup, mitm_verify # -import hid, sys, os +import hid, sys, os, platform from binascii import b2a_hex, a2b_hex from hashlib import sha256 from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN @@ -27,6 +27,8 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.is_simulator = False if not dev and sn and '/' in sn: + if platform.system() == 'Windows': + raise RuntimeError("Cannot connect to simulator. Is it running?") dev = UnixSimulatorPipe(sn) found = 'simulator' self.is_simulator = True From 6a6c46f6834b4dac5e67d0c92a9ef791619e9dce Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 15 Sep 2020 18:29:55 +0300 Subject: [PATCH 195/634] Allow passing None to BitBox2 passphrase Check for emptiness instead of an empty string when initializing BitBox2. --- hwilib/devices/bitbox02.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 04e7036f9..9222c8b86 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -246,7 +246,7 @@ def __init__(self, path: str, password: str = "", expert: bool = False) -> None: Initializes a new BitBox02 client instance. """ super().__init__(path, password=password, expert=expert) - if password != "": + if not password: raise BadArgumentError( "The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock." ) From a823e0445e6f1dfb9cd7a010e1d733b7d5917c05 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 7 Oct 2020 17:34:40 -0400 Subject: [PATCH 196/634] Create a supply wallet The automatic default wallet creation was removed, esxplicitly make one for our supply of coins. --- test/test_device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 25e40fe48..e723dfb8a 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -54,7 +54,9 @@ def cleanup_bitcoind(): pass # Make sure there are blocks and coins available - rpc.generatetoaddress(101, rpc.getnewaddress()) + rpc.createwallet(wallet_name="supply") + wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/supply'.format(userpass)) + wrpc.generatetoaddress(101, wrpc.getnewaddress()) return (rpc, userpass) class DeviceTestCase(unittest.TestCase): @@ -121,7 +123,7 @@ def setup_wallets(self): wallet_name = '{}_{}_test'.format(self.full_type, self.id()) self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=True, descriptors=True) self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}'.format(self.rpc_userpass, wallet_name)) - self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass)) + self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/supply'.format(self.rpc_userpass)) def setUp(self): self.emulator.start() From 191d840b7f4be1826aa94c5c10d7068942d327b9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 7 Oct 2020 19:39:54 -0400 Subject: [PATCH 197/634] Trezor uses poetry now --- test/setup_environment.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index acb9af26f..acbc6d012 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -36,10 +36,10 @@ fi cd legacy export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 if [ "$trezor_setup_needed" == true ] ; then - pipenv sync - pipenv run script/setup + poetry install + poetry run script/setup fi -pipenv run script/cibuild +poetry run script/cibuild # Delete any emulator.img file find . -name "emulator.img" -exec rm {} \; cd .. From d6e382a7cc1b95ec66a50096a42a980256f1b04b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 7 Oct 2020 19:47:32 -0400 Subject: [PATCH 198/634] Lock in to poetry 1.0.10 because it isn't broken --- .travis.yml | 2 +- contrib/build_bin.sh | 2 +- contrib/build_dist.sh | 2 +- contrib/build_wine.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a392623a9..3b84de4a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ before_install: mv travis-wait-enhanced /home/travis/bin/ travis-wait-enhanced --version install: - - pip install pipenv pysdl2 protobuf poetry construct mnemonic pyelftools + - pip install pipenv pysdl2 protobuf poetry==1.0.10 construct mnemonic pyelftools # 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 diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 99e2082e2..a96c72697 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -6,7 +6,7 @@ set -ex eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip -pip install poetry +pip install poetry==1.0.10 # Setup poetry and install the dependencies poetry install -E qt diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index c8bd7fd36..1c7c5ac19 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -6,7 +6,7 @@ set -ex eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip -pip install poetry +pip install poetry==1.0.10 # Setup poetry and install the dependencies poetry install -E qt diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 7b48b204e..c09a67b58 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -58,7 +58,7 @@ popd $PYTHON -m pip install -U pip # Install Poetry and things needed for pyinstaller -$PYTHON -m pip install poetry +$PYTHON -m pip install poetry==1.0.10 # We also need to change the timestamps of all of the base library files lib_dir=~/.wine/drive_c/python3/Lib From b8cf8578836cf92918c619f6f763c69697926810 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 4 Nov 2020 14:16:36 +0100 Subject: [PATCH 199/634] bitbox02: fix enumerate regression 6a6c46f6834b4dac5e67d0c92a9ef791619e9dce checks that a password must exist instead of that a password must not exist. That commit does not describe the reason for the change. Password cannot be None anyway, as the type of password is `str`, not `Optional[str]`. --- hwilib/devices/bitbox02.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 34e7630ca..edd20fb73 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -214,7 +214,7 @@ def __init__(self, path: str, password: str = "", expert: bool = False) -> None: Initializes a new BitBox02 client instance. """ super().__init__(path, password=password, expert=expert) - if not password: + if password: raise BadArgumentError( "The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock." ) From e1cc1937179a7fdb8e9807a0ef9e6f9b8ece8df0 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 18 Nov 2020 12:23:56 +0100 Subject: [PATCH 200/634] bitbox02: unlock during enumerate() Make it consistent with other HW wallets. Nunchuk relies on having an unlocked device after enumerate() finishes. --- hwilib/devices/bitbox02.py | 66 +++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 34e7630ca..aa15769ef 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -144,6 +144,10 @@ def enumerate(password: str = "") -> List[Dict[str, object]]: path = device_info["path"].decode() client = Bitbox02Client(path) client.set_noise_config(SilentNoiseConfig()) + d_data: Dict[str, object] = {} + bb02 = None + with handle_errors(common_err_msgs["enumerate"], d_data): + bb02 = client.init(expect_initialized=None) version, platform, edition, unlocked = bitbox02.BitBox02.get_info( client.transport ) @@ -156,25 +160,32 @@ def enumerate(password: str = "") -> List[Dict[str, object]]: assert isinstance(edition, BitBox02Edition) - d_data = { - "type": "bitbox02", - "path": path, - "model": { - BitBox02Edition.MULTI: "bitbox02_multi", - BitBox02Edition.BTCONLY: "bitbox02_btconly", - }[edition], - "needs_pin_sent": False, - "needs_passphrase_sent": False, - } + d_data.update( + { + "type": "bitbox02", + "path": path, + "model": { + BitBox02Edition.MULTI: "bitbox02_multi", + BitBox02Edition.BTCONLY: "bitbox02_btconly", + }[edition], + "needs_pin_sent": False, + "needs_passphrase_sent": False, + } + ) - with handle_errors(common_err_msgs["enumerate"], d_data): - if not unlocked: - raise DeviceNotReadyError( - "Please load wallet to unlock." - if _using_external_gui - else "Please use any subcommand to unlock" - ) - d_data["fingerprint"] = client.get_master_fingerprint_hex() + if bb02 is not None: + with handle_errors(common_err_msgs["enumerate"], d_data): + if not bb02.device_info()["initialized"]: + raise DeviceNotReadyError( + "BitBox02 is not initialized. Please initialize it using the BitBoxApp." + ) + elif not unlocked: + raise DeviceNotReadyError( + "Please load wallet to unlock." + if _using_external_gui + else "Please use any subcommand to unlock" + ) + d_data["fingerprint"] = client.get_master_fingerprint_hex() result.append(d_data) @@ -232,7 +243,7 @@ def __init__(self, path: str, password: str = "", expert: bool = False) -> None: def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None: self.noise_config = noise_config - def init(self, expect_initialized: bool = True) -> bitbox02.BitBox02: + def init(self, expect_initialized: Optional[bool] = True) -> bitbox02.BitBox02: if self.bb02 is not None: return self.bb02 @@ -250,14 +261,17 @@ def init(self, expect_initialized: bool = True) -> bitbox02.BitBox02: raise self.bb02 = bb02 is_initialized = bb02.device_info()["initialized"] - if expect_initialized: - if not is_initialized: - raise HWWError( - "The BitBox02 must be initialized first.", - DEVICE_NOT_INITIALIZED, + if expect_initialized is not None: + if expect_initialized: + if not is_initialized: + raise HWWError( + "The BitBox02 must be initialized first.", + DEVICE_NOT_INITIALIZED, + ) + elif is_initialized: + raise UnavailableActionError( + "The BitBox02 must be wiped before setup." ) - elif is_initialized: - raise UnavailableActionError("The BitBox02 must be wiped before setup.") return bb02 raise Exception( From a7407e268b817515fd78b1c2225753538410070a Mon Sep 17 00:00:00 2001 From: TamtamHero <10632523+TamtamHero@users.noreply.github.com> Date: Wed, 18 Nov 2020 13:07:12 +0100 Subject: [PATCH 201/634] Upgrade USB enumeration logic for Ledger products --- hwilib/devices/ledger.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 32bda0092..ee11e80e4 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -43,10 +43,10 @@ SIMULATOR_PATH = 'tcp:127.0.0.1:9999' LEDGER_VENDOR_ID = 0x2c97 -LEDGER_DEVICE_IDS = [ - 0x0001, # Ledger Nano S - 0x0004, # Ledger Nano X -] +LEDGER_MODEL_IDS = { + 0x10: "ledger_nano_s", + 0x40: "ledger_nano_x" +} # minimal checking of string keypath def check_keypath(key_path): @@ -386,9 +386,8 @@ def toggle_passphrase(self): def enumerate(password=''): results = [] devices = [] - for device_id in LEDGER_DEVICE_IDS: - devices.extend(hid.enumerate(LEDGER_VENDOR_ID, device_id)) - devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 1}) + devices.extend(hid.enumerate(LEDGER_VENDOR_ID, 0)) + devices.append({'path': SIMULATOR_PATH.encode(), 'interface_number': 0, 'product_id': 0x1000}) for d in devices: if ('interface_number' in d and d['interface_number'] == 0 @@ -397,7 +396,10 @@ def enumerate(password=''): path = d['path'].decode() d_data['type'] = 'ledger' - d_data['model'] = 'ledger_nano_x' if d['product_id'] == 0x0004 else 'ledger_nano_s' + model = d['product_id'] >> 8 + if model not in LEDGER_MODEL_IDS.keys(): + continue + d_data['model'] = LEDGER_MODEL_IDS[model] d_data['path'] = path if path == SIMULATOR_PATH: From 1080756258477b15c4e61025635e5b7b2dc947d4 Mon Sep 17 00:00:00 2001 From: TamtamHero <10632523+TamtamHero@users.noreply.github.com> Date: Wed, 18 Nov 2020 13:17:09 +0100 Subject: [PATCH 202/634] Add back support for Ledger legacy USB product IDs --- hwilib/devices/ledger.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index ee11e80e4..2edec283f 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -47,6 +47,10 @@ 0x10: "ledger_nano_s", 0x40: "ledger_nano_x" } +LEDGER_LEGACY_PRODUCT_IDS = { + 0x0001: "ledger_nano_s", + 0x0004: "ledger_nano_x" +} # minimal checking of string keypath def check_keypath(key_path): @@ -397,9 +401,12 @@ def enumerate(password=''): path = d['path'].decode() d_data['type'] = 'ledger' model = d['product_id'] >> 8 - if model not in LEDGER_MODEL_IDS.keys(): + if model in LEDGER_MODEL_IDS.keys(): + d_data['model'] = LEDGER_MODEL_IDS[model] + elif d['product_id'] in LEDGER_LEGACY_PRODUCT_IDS.keys(): + d_data['model'] = LEDGER_LEGACY_PRODUCT_IDS[d['product_id']] + else: continue - d_data['model'] = LEDGER_MODEL_IDS[model] d_data['path'] = path if path == SIMULATOR_PATH: From 98967d1c65571a0595d15738522e2f2e169484df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Moln=C3=A1r?= Date: Tue, 1 Dec 2020 12:56:17 +0100 Subject: [PATCH 203/634] Pastable udev Shell script --- hwilib/udev/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/hwilib/udev/README.md b/hwilib/udev/README.md index 57d7a9534..b3078d78f 100644 --- a/hwilib/udev/README.md +++ b/hwilib/udev/README.md @@ -14,11 +14,12 @@ These are necessary for the devices to be reachable on linux environments. Apply these rules by copying them to `/etc/udev/rules.d/` and notifying `udevadm`. Your user will need to be added to the `plugdev` group, which needs to be created if it does not already exist. +```Shell +cd hwilib/; \ + sudo cp udev/*.rules /etc/udev/rules.d/ && \ + sudo udevadm trigger && \ + sudo udevadm control --reload-rules && \ + sudo groupadd plugdev && \ + sudo usermod -aG plugdev `whoami` ``` -$ cd hwilib/ -$ sudo cp udev/*.rules /etc/udev/rules.d/ -$ sudo udevadm trigger -$ sudo udevadm control --reload-rules -$ sudo groupadd plugdev -$ sudo usermod -aG plugdev `whoami` -``` + From 9a6b1f3c7955015c449e6bcce94df8bcd6afe7ed Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 15:31:04 -0400 Subject: [PATCH 204/634] Add is_hardened helper --- hwilib/key.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hwilib/key.py b/hwilib/key.py index 21b1f49a7..de9fcf245 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -21,6 +21,12 @@ def H_(x: int) -> int: """ return x | HARDENED_FLAG +def is_hardened(i: int) -> bool: + """ + Returns whether an index is hardened + """ + return i & HARDENED_FLAG != 0 + # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else @@ -102,7 +108,7 @@ def serialize(self) -> bytes: def _path_string(self) -> str: s = "" for i in self.path: - hardened = i & HARDENED_FLAG != 0 + hardened = is_hardened(i) i &= ~HARDENED_FLAG s += "/" + str(i) if hardened: From 539283e6e567b76787abb2cfcac5d3e7a801c04c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 16:08:06 -0400 Subject: [PATCH 205/634] Change ExtendedKey.deserialize a classmethod that creates ExtendedKey --- hwilib/devices/coldcard.py | 3 +- hwilib/devices/digitalbitbox.py | 3 +- hwilib/devices/ledger.py | 3 +- hwilib/devices/trezor.py | 6 ++-- hwilib/key.py | 53 +++++++++++++++++---------------- 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 39fe6a3a8..a196f90d9 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -117,8 +117,7 @@ def get_pubkey_at_path(self, path): else: result = {'xpub': xpub} if self.expert: - xpub_obj = ExtendedKey() - xpub_obj.deserialize(xpub) + xpub_obj = ExtendedKey.deserialize(xpub) result.update(xpub_obj.get_printable_dict()) return result diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 435b0cf09..466430d45 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -355,8 +355,7 @@ def get_pubkey_at_path(self, path): else: result = {'xpub': reply['xpub']} if self.expert: - xpub_obj = ExtendedKey() - xpub_obj.deserialize(reply['xpub']) + xpub_obj = ExtendedKey.deserialize(reply['xpub']) result.update(xpub_obj.get_printable_dict()) return result diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 2edec283f..3880ccee9 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -167,8 +167,7 @@ def get_pubkey_at_path(self, path): result = {"xpub": xpub} if self.expert: - xpub_obj = ExtendedKey() - xpub_obj.deserialize(xpub) + xpub_obj = ExtendedKey.deserialize(xpub) result.update(xpub_obj.get_printable_dict()) return result diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 3aeb489af..1b6a1476b 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -176,8 +176,7 @@ def get_pubkey_at_path(self, path): else: result = {'xpub': output.xpub} if self.expert: - xpub_obj = ExtendedKey() - xpub_obj.deserialize(output.xpub) + xpub_obj = ExtendedKey.deserialize(output.xpub) result.update(xpub_obj.get_printable_dict()) return result @@ -419,9 +418,8 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc # descriptor means multisig with xpubs if descriptor: pubkeys = [] - xpub = ExtendedKey() for i in range(0, descriptor.multisig_N): - xpub.deserialize(descriptor.base_key[i]) + xpub = ExtendedKey.deserialize(descriptor.base_key[i]) hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=parse_path('m' + descriptor.path_suffix[i]))) multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys) diff --git a/hwilib/key.py b/hwilib/key.py index de9fcf245..b8550119f 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -9,6 +9,7 @@ import struct from typing import ( Dict, + Optional, Sequence, ) @@ -37,34 +38,34 @@ class ExtendedKey(object): TESTNET_PUBLIC = b'\x04\x35\x87\xCF' TESTNET_PRIVATE = b'\x04\x35\x83\x94' - def __init__(self) -> None: - self.is_testnet: bool = False - self.is_private: bool = False - self.depth: int = 0 - self.parent_fingerprint: bytes = b'' - self.child_num: int = 0 - self.chaincode: bytes = b'' - self.pubkey: bytes = b'' - self.privkey: bytes = b'' - - def deserialize(self, xpub: str) -> None: + def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: Optional[bytes]) -> None: + self.version: bytes = version + self.is_testnet: bool = version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE + self.is_private: bool = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE + self.depth: int = depth + self.parent_fingerprint: bytes = parent_fingerprint + self.child_num: int = child_num + self.chaincode: bytes = chaincode + self.pubkey: Optional[bytes] = pubkey + self.privkey: Optional[bytes] = privkey + + @classmethod + def deserialize(cls, xpub: str) -> 'ExtendedKey': data = base58.decode(xpub)[:-4] # Decoded xpub without checksum version = data[0:4] - if version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE: - self.is_testnet = True - if version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE: - self.is_private = True - - self.depth = data[4] - self.parent_fingerprint = data[5:9] - self.child_num = struct.unpack('>I', data[9:13])[0] - self.chaincode = data[13:45] - - if self.is_private: - self.privkey = data[46:] + is_private = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE + depth = data[4] + parent_fingerprint = data[5:9] + child_num = struct.unpack('>I', data[9:13])[0] + chaincode = data[13:45] + + if is_private: + privkey = data[46:] + return cls(version, depth, parent_fingerprint, child_num, chaincode, privkey, None) else: - self.pubkey = data[45:78] + pubkey = data[45:78] + return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey) def get_printable_dict(self) -> Dict[str, object]: d: Dict[str, object] = {} @@ -74,9 +75,9 @@ def get_printable_dict(self) -> Dict[str, object]: d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode() d['child_num'] = self.child_num d['chaincode'] = binascii.hexlify(self.chaincode).decode() - if self.is_private: + if self.is_private and isinstance(self.privkey, bytes): d['privkey'] = binascii.hexlify(self.privkey).decode() - else: + elif isinstance(self.pubkey, bytes): d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d From 94719476f0b951e5a7c58db261fc8736a432db4b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 18:14:05 -0400 Subject: [PATCH 206/634] Add ECDSA point things Taken from bip32.py that was removed in a3cdd53ddaab86cf9b7cb12f73bc99a081b450a6 --- hwilib/key.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index b8550119f..68c408a13 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -11,10 +11,16 @@ Dict, Optional, Sequence, + Tuple, ) HARDENED_FLAG = 1 << 31 +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) + +Point = Optional[Tuple[int, int]] def H_(x: int) -> int: """ @@ -29,6 +35,52 @@ def is_hardened(i: int) -> bool: return i & HARDENED_FLAG != 0 +def point_add(p1: Point, p2: Point) -> Point: + if (p1 is None): + return p2 + if (p2 is None): + return p1 + if (p1[0] == p2[0] and p1[1] != p2[1]): + return None + if (p1 == p2): + lam = (3 * p1[0] * p1[0] * pow(2 * p1[1], p - 2, p)) % p + else: + lam = ((p2[1] - p1[1]) * pow(p2[0] - p1[0], p - 2, p)) % p + x3 = (lam * lam - p1[0] - p2[0]) % p + return (x3, (lam * (p1[0] - x3) - p1[1]) % p) + + +def point_mul(p: Point, n: int) -> Point: + r = None + for i in range(256): + if ((n >> i) & 1): + r = point_add(r, p) + p = point_add(p, p) + return r + + +def deserialize_point(b: bytes) -> Point: + x = int.from_bytes(b[1:], byteorder="big") + y = pow((x * x * x + 7) % p, (p + 1) // 4, p) + if (y & 1 != b[0] & 1): + y = p - y + return (x, y) + + +def bytes_to_point(point_bytes: bytes) -> Point: + header = point_bytes[0] + if header == 4: + x = point_bytes = point_bytes[1:33] + y = point_bytes = point_bytes[33:65] + return (int(binascii.hexlify(x), 16), int(binascii.hexlify(y), 16)) + return deserialize_point(point_bytes) + +def point_to_bytes(p: Point) -> bytes: + if p is None: + raise ValueError("Cannot convert None to bytes") + return (b'\x03' if p[1] & 1 else b'\x02') + p[0].to_bytes(32, byteorder="big") + + # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): From 42106a74ac06f3457f97f207b28e947030283a7a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 18:17:16 -0400 Subject: [PATCH 207/634] Compute the pubkey for a privkey --- hwilib/key.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hwilib/key.py b/hwilib/key.py index 68c408a13..1f208c0a7 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -90,7 +90,7 @@ class ExtendedKey(object): TESTNET_PUBLIC = b'\x04\x35\x87\xCF' TESTNET_PRIVATE = b'\x04\x35\x83\x94' - def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: Optional[bytes]) -> None: + def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: bytes) -> None: self.version: bytes = version self.is_testnet: bool = version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE self.is_private: bool = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE @@ -98,7 +98,7 @@ def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_ self.parent_fingerprint: bytes = parent_fingerprint self.child_num: int = child_num self.chaincode: bytes = chaincode - self.pubkey: Optional[bytes] = pubkey + self.pubkey: bytes = pubkey self.privkey: Optional[bytes] = privkey @classmethod @@ -114,7 +114,8 @@ def deserialize(cls, xpub: str) -> 'ExtendedKey': if is_private: privkey = data[46:] - return cls(version, depth, parent_fingerprint, child_num, chaincode, privkey, None) + pubkey = point_to_bytes(point_mul(G, int.from_bytes(privkey, byteorder="big"))) + return cls(version, depth, parent_fingerprint, child_num, chaincode, privkey, pubkey) else: pubkey = data[45:78] return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey) @@ -129,7 +130,7 @@ def get_printable_dict(self) -> Dict[str, object]: d['chaincode'] = binascii.hexlify(self.chaincode).decode() if self.is_private and isinstance(self.privkey, bytes): d['privkey'] = binascii.hexlify(self.privkey).decode() - elif isinstance(self.pubkey, bytes): + else: d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d From d3941a0b4db4c2f8c1d4426ed86c75aad5e031ea Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 16:11:49 -0400 Subject: [PATCH 208/634] Implement pubkey derivation in ExtendedKey --- hwilib/key.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 1f208c0a7..1acce507f 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -6,6 +6,8 @@ from . import base58 import binascii +import hmac +import hashlib import struct from typing import ( Dict, @@ -14,6 +16,7 @@ Tuple, ) + HARDENED_FLAG = 1 << 31 p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F @@ -134,6 +137,34 @@ def get_printable_dict(self) -> Dict[str, object]: d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d + def derive_pub(self, i: int) -> 'ExtendedKey': + if is_hardened(i): + raise ValueError("Index cannot be larger than 2^31") + + # Data to HMAC. Same as CKDpriv() for public child key. + data = self.pubkey + struct.pack(">L", i) + + # Get HMAC of data + Ihmac = hmac.new(self.chaincode, data, hashlib.sha512).digest() + Il = Ihmac[:32] + Ir = Ihmac[32:] + + # Construct curve point Il*G+K + Il_int = int(binascii.hexlify(Il), 16) + child_pubkey = point_add(point_mul(G, Il_int), bytes_to_point(self.pubkey)) + + # Construct and return a new BIP32Key + pubkey = point_to_bytes(child_pubkey) + chaincode = Ir + fingerprint = hashlib.new('ripemd160', hashlib.sha256(self.pubkey).digest()).digest()[0:4] + return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth + 1, fingerprint, i, chaincode, None, pubkey) + + def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey': + key = self + for i in path: + key = key.derive_pub(i) + return key + class KeyOriginInfo(object): def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: From b4730c3c67394ccf8936113e1259ecb3d18faedc Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 18:06:49 -0400 Subject: [PATCH 209/634] Always output pubkey in ExtendedKey printable dict --- hwilib/key.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hwilib/key.py b/hwilib/key.py index 1acce507f..00469cc1e 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -133,8 +133,7 @@ def get_printable_dict(self) -> Dict[str, object]: d['chaincode'] = binascii.hexlify(self.chaincode).decode() if self.is_private and isinstance(self.privkey, bytes): d['privkey'] = binascii.hexlify(self.privkey).decode() - else: - d['pubkey'] = binascii.hexlify(self.pubkey).decode() + d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d def derive_pub(self, i: int) -> 'ExtendedKey': From 07485ebabf17e1f5a3d7f9665ea4647731d992a3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 18:08:50 -0400 Subject: [PATCH 210/634] Add serialize and to_string methods to ExtendedKey --- hwilib/key.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 00469cc1e..409eb8531 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -123,6 +123,21 @@ def deserialize(cls, xpub: str) -> 'ExtendedKey': pubkey = data[45:78] return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey) + def serialize(self) -> bytes: + r = self.version + struct.pack('B', self.depth) + self.parent_fingerprint + struct.pack('>I', self.child_num) + self.chaincode + if self.is_private: + if self.privkey is None: + raise ValueError("Somehow we are private but don't have a privkey") + r += b"\x00" + self.privkey + else: + r += self.pubkey + return r + + def to_string(self) -> str: + data = self.serialize() + checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()[0:4] + return base58.encode(data + checksum) + def get_printable_dict(self) -> Dict[str, object]: d: Dict[str, object] = {} d['testnet'] = self.is_testnet From 7edbdb95fdaee71093c87fda2cdec56eb6dfd053 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 18 Aug 2020 18:18:10 -0400 Subject: [PATCH 211/634] Add test for ExtendedKey --- test/data/test_bip32.json | 260 ++++++++++++++++++++++++++++++++++++++ test/run_tests.py | 2 + test/test_bip32.py | 127 +++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 test/data/test_bip32.json create mode 100755 test/test_bip32.py diff --git a/test/data/test_bip32.json b/test/data/test_bip32.json new file mode 100644 index 000000000..1e2488be4 --- /dev/null +++ b/test/data/test_bip32.json @@ -0,0 +1,260 @@ +{ + "serialization": [ + { + "xpub": "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + "xprv": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 0, + "parent_fingerprint": "00000000", + "child_num": 0, + "chaincode": "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508", + "pubkey": "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2", + "privkey": "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + } + }, + { + "xpub": "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", + "xprv": "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 1, + "parent_fingerprint": "3442193e", + "child_num": 2147483648, + "chaincode": "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141", + "pubkey": "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56", + "privkey": "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea" + } + }, + { + "xpub": "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", + "xprv": "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 2, + "parent_fingerprint": "5c1bd648", + "child_num": 1, + "chaincode": "2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19", + "pubkey": "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c", + "privkey": "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368" + } + }, + { + "xpub": "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + "xprv": "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 3, + "parent_fingerprint": "bef5a2f9", + "child_num": 2147483650, + "chaincode": "04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f", + "pubkey": "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2", + "privkey": "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca" + } + }, + { + "xpub": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + "xprv": "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 4, + "parent_fingerprint": "ee7ab90c", + "child_num": 2, + "chaincode": "cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd", + "pubkey": "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29", + "privkey": "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4" + } + }, + { + "xpub": "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + "xprv": "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 5, + "parent_fingerprint": "d880d7d8", + "child_num": 1000000000, + "chaincode": "c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e", + "pubkey": "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011", + "privkey": "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8" + } + }, + { + "xpub": "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + "xprv": "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 0, + "parent_fingerprint": "00000000", + "child_num": 0, + "chaincode": "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689", + "pubkey": "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7", + "privkey": "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e" + } + }, + { + "xpub": "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + "xprv": "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 1, + "parent_fingerprint": "bd16bee5", + "child_num": 0, + "chaincode": "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c", + "pubkey": "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea", + "privkey": "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e" + } + }, + { + "xpub": "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", + "xprv": "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 2, + "parent_fingerprint": "5a61ff8e", + "child_num": 4294967295, + "chaincode": "be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9", + "pubkey": "03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b", + "privkey": "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93" + } + }, + { + "xpub": "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", + "xprv": "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 3, + "parent_fingerprint": "d8ab4937", + "child_num": 1, + "chaincode": "f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb", + "pubkey": "03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9", + "privkey": "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7" + } + }, + { + "xpub": "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + "xprv": "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 4, + "parent_fingerprint": "78412e3a", + "child_num": 4294967294, + "chaincode": "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29", + "pubkey": "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0", + "privkey": "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d" + } + }, + { + "xpub": "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", + "xprv": "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 5, + "parent_fingerprint": "31a507b8", + "child_num": 2, + "chaincode": "9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271", + "pubkey": "024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c", + "privkey": "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23" + } + }, + { + "xpub": "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", + "xprv": "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 0, + "parent_fingerprint": "00000000", + "child_num": 0, + "chaincode": "01d28a3e53cffa419ec122c968b3259e16b65076495494d97cae10bbfec3c36f", + "pubkey": "03683af1ba5743bdfc798cf814efeeab2735ec52d95eced528e692b8e34c4e5669", + "privkey": "00ddb80b067e0d4993197fe10f2657a844a384589847602d56f0c629c81aae32" + } + }, + { + "xpub": "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", + "xprv": "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", + "deser": { + "pub_version": "0488B21E", + "priv_version": "0488ADE4", + "is_testnet": false, + "depth": 1, + "parent_fingerprint": "41d63b50", + "child_num": 2147483648, + "chaincode": "e5fea12a97b927fc9dc3d2cb0d1ea1cf50aa5a1fdc1f933e8906bb38df3377bd", + "pubkey": "026557fdda1d5d43d79611f784780471f086d58e8126b8c40acb82272a7712e7f2", + "privkey": "491f7a2eebc7b57028e0d3faa0acda02e75c33b03c48fb288c41e2ea44e1daef" + } + } + ], + "deriv" : [ + { + "parent_xpub": "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", + "parent_xprv": "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", + "child_xpub": "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", + "index": 1 + }, + { + "parent_xpub": "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + "parent_xprv": "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", + "child_xpub": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + "index": 2 + }, + { + "parent_xpub": "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + "parent_xprv": "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", + "child_xpub": "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + "index": 1000000000 + }, + { + "parent_xpub": "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + "parent_xprv": "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + "child_xpub": "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + "index": 0 + }, + { + "parent_xpub": "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", + "parent_xprv": "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", + "child_xpub": "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", + "index": 1 + }, + { + "parent_xpub": "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + "parent_xprv": "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", + "child_xpub": "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", + "index": 2 + } + ], + "deriv_path": [ + { + "parent_xpub": "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + "parent_xprv": "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", + "child_xpub": "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + "path": "m/2/1000000000" + } + ] +} diff --git a/test/run_tests.py b/test/run_tests.py index e3969dea8..d1d2dbffa 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -6,6 +6,7 @@ from test_base58 import TestBase58 from test_bech32 import TestSegwitAddress +from test_bip32 import TestBIP32 from test_coldcard import coldcard_test_suite from test_descriptor import TestDescriptor from test_device import start_bitcoind @@ -62,6 +63,7 @@ suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) +suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBIP32)) if sys.platform.startswith("linux"): suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() diff --git a/test/test_bip32.py b/test/test_bip32.py new file mode 100755 index 000000000..5a88d8243 --- /dev/null +++ b/test/test_bip32.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The HWI developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from hwilib.key import ( + ExtendedKey, + parse_path, +) + +import binascii +import json +import os +import unittest + +class TestBIP32(unittest.TestCase): + @classmethod + def setUpClass(cls): + with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/test_bip32.json"), encoding="utf-8") as f: + cls.data = json.load(f) + for key in cls.data["serialization"]: + deser = key["deser"] + deser["pub_version"] = binascii.unhexlify(deser["pub_version"]) + deser["priv_version"] = binascii.unhexlify(deser["priv_version"]) + deser["hex_parent_fingerprint"] = deser["parent_fingerprint"] + deser["parent_fingerprint"] = binascii.unhexlify(deser["parent_fingerprint"]) + deser["hex_chaincode"] = deser["chaincode"] + deser["chaincode"] = binascii.unhexlify(deser["chaincode"]) + deser["hex_pubkey"] = deser["pubkey"] + deser["pubkey"] = binascii.unhexlify(deser["pubkey"]) + deser["hex_privkey"] = deser["privkey"] + deser["privkey"] = binascii.unhexlify(deser["privkey"]) + + def test_serialization(self): + for key in self.data["serialization"]: + xpub = key["xpub"] + xprv = key["xprv"] + deser = key["deser"] + with self.subTest(key=key): + key_pub = ExtendedKey.deserialize(xpub) + key_prv = ExtendedKey.deserialize(xprv) + + # Make sure they roundtrip + self.assertEqual(key_pub.to_string(), xpub) + self.assertEqual(key_prv.to_string(), xprv) + + # Make sure they agree + self.assertEqual(key_pub.is_testnet, key_prv.is_testnet) + self.assertEqual(key_pub.depth, key_prv.depth) + self.assertEqual(key_pub.parent_fingerprint, key_prv.parent_fingerprint) + self.assertEqual(key_pub.child_num, key_prv.child_num) + self.assertEqual(key_pub.chaincode, key_prv.chaincode) + self.assertEqual(key_pub.pubkey, key_prv.pubkey) + + # Make sure they are correct + self.assertEqual(key_pub.version, deser["pub_version"]) + self.assertEqual(key_pub.is_testnet, deser["is_testnet"]) + self.assertEqual(key_pub.is_private, False) + self.assertEqual(key_pub.depth, deser["depth"]) + self.assertEqual(key_pub.parent_fingerprint, deser["parent_fingerprint"]) + self.assertEqual(key_pub.child_num, deser["child_num"]) + self.assertEqual(key_pub.chaincode, deser["chaincode"]) + self.assertEqual(key_pub.pubkey, deser["pubkey"]) + self.assertEqual(key_prv.version, deser["priv_version"]) + self.assertEqual(key_prv.is_testnet, deser["is_testnet"]) + self.assertEqual(key_prv.is_private, True) + self.assertEqual(key_prv.depth, deser["depth"]) + self.assertEqual(key_prv.parent_fingerprint, deser["parent_fingerprint"]) + self.assertEqual(key_prv.child_num, deser["child_num"]) + self.assertEqual(key_prv.chaincode, deser["chaincode"]) + self.assertEqual(key_prv.pubkey, deser["pubkey"]) + self.assertEqual(key_prv.privkey, deser["privkey"]) + + # Make sure the printable dict is right + key_dict = key_pub.get_printable_dict() + self.assertEqual(key_dict["testnet"], deser["is_testnet"]) + self.assertEqual(key_dict["private"], False) + self.assertEqual(key_dict["depth"], deser["depth"]) + self.assertEqual(key_dict["parent_fingerprint"], deser["hex_parent_fingerprint"]) + self.assertEqual(key_dict["child_num"], deser["child_num"]) + self.assertEqual(key_dict["chaincode"], deser["hex_chaincode"]) + self.assertEqual(key_dict["pubkey"], deser["hex_pubkey"]) + key_dict = key_prv.get_printable_dict() + self.assertEqual(key_dict["testnet"], deser["is_testnet"]) + self.assertEqual(key_dict["private"], True) + self.assertEqual(key_dict["depth"], deser["depth"]) + self.assertEqual(key_dict["parent_fingerprint"], deser["hex_parent_fingerprint"]) + self.assertEqual(key_dict["child_num"], deser["child_num"]) + self.assertEqual(key_dict["chaincode"], deser["hex_chaincode"]) + self.assertEqual(key_dict["pubkey"], deser["hex_pubkey"]) + self.assertEqual(key_dict["privkey"], deser["hex_privkey"]) + + def test_deriv(self): + for test in self.data["deriv"]: + with self.subTest(test=test): + # Deser + par_xpub = ExtendedKey.deserialize(test["parent_xpub"]) + par_xprv = ExtendedKey.deserialize(test["parent_xprv"]) + + # Derive + i = test["index"] + child_xpub = test["child_xpub"] + xpub_der = par_xpub.derive_pub(i) + self.assertEqual(xpub_der.to_string(), child_xpub) + xprv_der = par_xprv.derive_pub(i) + self.assertEqual(xprv_der.to_string(), child_xpub) + + def test_deriv_path(self): + for test in self.data["deriv_path"]: + with self.subTest(test=test): + # Deser + par_xpub = ExtendedKey.deserialize(test["parent_xpub"]) + par_xprv = ExtendedKey.deserialize(test["parent_xprv"]) + + # Parse the path + path = parse_path(test["path"]) + + # Derive + child_xpub = test["child_xpub"] + xpub_der = par_xpub.derive_pub_path(path) + self.assertEqual(xpub_der.to_string(), child_xpub) + xprv_der = par_xprv.derive_pub_path(path) + self.assertEqual(xprv_der.to_string(), child_xpub) + + +if __name__ == "__main__": + unittest.main() From cdbdb23520a4d8190c1b7f3de8094453ebf4c5cb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 21:26:03 -0400 Subject: [PATCH 212/634] Add KeyOriginInfo.from_string --- hwilib/key.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 409eb8531..8300711df 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -223,6 +223,21 @@ def to_string(self) -> str: s += self._path_string() return s + @classmethod + def from_string(cls, s: str) -> 'KeyOriginInfo': + """ + Create a KeyOriginInfo from the string + + :param s: The string to parse + """ + s = s.lower() + entries = s.split("/") + fingerprint = binascii.unhexlify(s[0:8]) + path: Sequence[int] = [] + if len(entries) > 1: + path = parse_path(s[9:]) + return cls(fingerprint, path) + def get_derivation_path(self) -> str: """ Return the string for just the path From eed2fb098c5144ea00e9dad45df25f89e2a5c6b0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 02:24:53 -0400 Subject: [PATCH 213/634] Re-implement Descriptor similar to how Core does it --- hwilib/commands.py | 136 ++++++++++----- hwilib/descriptor.py | 369 ++++++++++++++++++++++----------------- hwilib/devices/trezor.py | 8 +- hwilib/key.py | 6 + test/test_descriptor.py | 171 ++++++++---------- 5 files changed, 380 insertions(+), 310 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 8ffcd3020..54b41150f 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -2,20 +2,41 @@ # Hardware wallet interaction script +import binascii import importlib import platform from .serializations import PSBT from .base58 import xpub_to_pub_hex +from .key import ( + H_, + HARDENED_FLAG, + is_hardened, + KeyOriginInfo, + parse_path, +) from .errors import ( UnknownDeviceError, UnavailableActionError, BAD_ARGUMENT, NOT_IMPLEMENTED, ) -from .descriptor import Descriptor +from .descriptor import ( + Descriptor, + parse_descriptor, + MultisigDescriptor, + PKHDescriptor, + PubkeyProvider, + SHDescriptor, + WPKHDescriptor, + WSHDescriptor, +) from .devices import __all__ as all_devs + from enum import Enum +from itertools import count + +py_enumerate = enumerate class AddressType(Enum): PKH = 1 @@ -106,7 +127,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import = {} - this_import['desc'] = desc.serialize() + this_import['desc'] = desc.to_string() this_import['range'] = [start, end] this_import['timestamp'] = 'now' this_import['internal'] = internal @@ -121,53 +142,65 @@ def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, is_wpkh = addr_type is AddressType.WPKH is_sh_wpkh = addr_type is AddressType.SH_WPKH + parsed_path = [] if not path: - # Master key: - path = "m/" - # Purpose if is_wpkh: - path += "84'/" + parsed_path.append(H_(84)) elif is_sh_wpkh: - path += "49'/" + parsed_path.append(H_(49)) else: assert addr_type == AddressType.PKH - path += "44'/" + parsed_path.append(H_(44)) # Coin type if testnet: - path += "1'/" + parsed_path.append(H_(1)) else: - path += "0'/" + parsed_path.append(H_(0)) # Account - path += str(account) + '\'/' + parsed_path.append(H_(account)) # Receive or change if internal: - path += "1/*" + parsed_path.append(1) else: - path += "0/*" + parsed_path.append(0) else: if path[0] != "m": return {'error': 'Path must start with m/', 'code': BAD_ARGUMENT} if path[-1] != "*": return {'error': 'Path must end with /*', 'code': BAD_ARGUMENT} + parsed_path = parse_path(path[:-2]) # Find the last hardened derivation: - path = path.replace('\'', 'h') - path_suffix = '' - for component in path.split("/")[::-1]: - if component[-1] == 'h' or component[-1] == 'm': + for i, p in zip(count(len(parsed_path) - 1, -1), reversed(parsed_path)): + if is_hardened(p): break - path_suffix = '/' + component + path_suffix - path_base = path.rsplit(path_suffix)[0] + i += 1 + + origin = KeyOriginInfo(binascii.unhexlify(master_fpr), parsed_path[:i]) + path_base = origin.get_derivation_path() + + path_suffix = "" + for p in parsed_path[i:]: + hardened = is_hardened(p) + p &= ~HARDENED_FLAG + path_suffix += "/{}{}".format(p, "h" if hardened else "") + path_suffix += "/*" # Get the key at the base if client.xpub_cache.get(path_base) is None: client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base)['xpub'] - return Descriptor(master_fpr, path_base.replace('m', ''), client.xpub_cache.get(path_base), path_suffix, client.is_testnet, is_sh_wpkh, is_wpkh) + pubkey = PubkeyProvider(origin, client.xpub_cache.get(path_base), path_suffix) + if is_wpkh: + return WPKHDescriptor(pubkey) + elif is_sh_wpkh: + return SHDescriptor(WPKHDescriptor(pubkey)) + else: + return PKHDescriptor(pubkey) def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, sh_wpkh=False, wpkh=True, addr_all=False): @@ -216,7 +249,7 @@ def getdescriptors(client, account=0): continue if not isinstance(desc, Descriptor): return desc - descriptors.append(desc.serialize()) + descriptors.append(desc.to_string()) if internal: result["internal"] = descriptors else: @@ -234,32 +267,41 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede return {'error': ' `--wpkh` and `--sh_wpkh` can not be combined with --desc', 'code': BAD_ARGUMENT} if redeem_script: return {'error': ' `--redeem_script` can not be combined with --desc', 'code': BAD_ARGUMENT} - descriptor = Descriptor.parse(desc, client.is_testnet) - if descriptor is None: - return {'error': 'Unable to parse descriptor: ' + desc, 'code': BAD_ARGUMENT} - if descriptor.sh or descriptor.sh_wsh or descriptor.wsh: - path = '' - redeem_script = format(80 + int(descriptor.multisig_M), 'x') - xpubs_descriptor = False - for i in range(0, descriptor.multisig_N): - path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] - if not descriptor.path_suffix[i]: - redeem_script += '21' + descriptor.base_key[i] - else: - path += descriptor.path_suffix[i] - xpubs_descriptor = True - path += ',' - path = path[0:-1] - redeem_script += format(80 + descriptor.multisig_N, 'x') + 'ae' - return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None) - if descriptor.m_path is None: - return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} - if descriptor.origin_fingerprint != client.get_master_fingerprint_hex(): - return {'error': 'Descriptor fingerprint does not match device: ' + desc, 'code': BAD_ARGUMENT} - xpub = client.get_pubkey_at_path(descriptor.m_path_base)['xpub'] - if descriptor.base_key != xpub and descriptor.base_key != xpub_to_pub_hex(xpub): - return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} - return client.display_address(descriptor.m_path, descriptor.sh_wpkh, descriptor.wpkh) + descriptor = parse_descriptor(desc) + is_sh = isinstance(descriptor, SHDescriptor) + is_wsh = isinstance(descriptor, WSHDescriptor) + if is_sh or is_wsh: + descriptor = descriptor.subdescriptor + if isinstance(descriptor, WSHDescriptor): + is_wsh = True + descriptor = descriptor.subdescriptor + if isinstance(descriptor, MultisigDescriptor): + path = '' + redeem_script = format(80 + int(descriptor.thresh), 'x') + xpubs_descriptor = False + for p in descriptor.pubkeys: + path += p.origin.to_string() + if not p.deriv_path: + redeem_script += format(len(p.pubkey) // 2, 'x') + redeem_script += p.pubkey + else: + path += p.deriv_path + xpubs_descriptor = True + path += ',' + path = path[0:-1] + redeem_script += format(80 + len(descriptor.pubkeys), 'x') + 'ae' + return client.display_address(path, is_sh and is_wsh, not is_sh and is_wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None) + is_wpkh = isinstance(descriptor, WPKHDescriptor) + if isinstance(descriptor, PKHDescriptor) or is_wpkh: + pubkey = descriptor.pubkeys[0] + if pubkey.origin is None: + return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} + if pubkey.origin.get_fingerprint_hex() != client.get_master_fingerprint_hex(): + return {'error': 'Descriptor fingerprint does not match device: ' + desc, 'code': BAD_ARGUMENT} + xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path())['xpub'] + if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): + return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} + return client.display_address(pubkey.origin.get_derivation_path(), is_sh and is_wpkh, not is_sh and is_wpkh) def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index d5c3cf1b8..5d56bd86f 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,9 +1,15 @@ -# mypy: ignore-errors -import re +from .key import KeyOriginInfo + +from enum import Enum +from typing import ( + List, + Optional, + Tuple, +) # From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp -def PolyMod(c, val): +def PolyMod(c: int, val: int) -> int: c0 = c >> 35 c = ((c & 0x7ffffffff) << 5) ^ val if (c0 & 1): @@ -18,7 +24,7 @@ def PolyMod(c, val): c ^= 0x644d626ffd return c -def DescriptorChecksum(desc): +def DescriptorChecksum(desc: str) -> str: INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -42,174 +48,207 @@ def DescriptorChecksum(desc): c = PolyMod(c, 0) c ^= 1 - ret = [None] * 8 + ret = [''] * 8 for j in range(0, 8): ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] return ''.join(ret) -def AddChecksum(desc): +def AddChecksum(desc: str) -> str: return desc + "#" + DescriptorChecksum(desc) -class Descriptor: + +class PubkeyProvider(object): def __init__( self, - origin_fingerprint, - origin_path, - base_key, - path_suffix, - testnet, - sh_wpkh=None, - wpkh=None, - sh=None, - sh_wsh=None, - wsh=None, - multisig_M=None, - multisig_N=None - ): - self.origin_fingerprint = origin_fingerprint - self.origin_path = origin_path - self.path_suffix = path_suffix - self.base_key = base_key - self.testnet = testnet - self.sh_wpkh = sh_wpkh - self.wpkh = wpkh - self.sh = sh - self.sh_wsh = sh_wsh - self.wsh = wsh - self.multisig_M = multisig_M - self.multisig_N = multisig_N - self.m_path = None - - if origin_path and not isinstance(origin_path, list): - self.m_path_base = "m" + origin_path - self.m_path = "m" + origin_path + (path_suffix or "") - elif isinstance(origin_path, list): - self.m_path_base = [] - self.m_path = [] - for i in range(0, len(origin_path)): - self.m_path_base.append("m" + origin_path[i]) - self.m_path.append("m" + origin_path[i] + (path_suffix[i] or "")) + origin: Optional['KeyOriginInfo'], + pubkey: str, + deriv_path: Optional[str] + ) -> None: + self.origin = origin + self.pubkey = pubkey + self.deriv_path = deriv_path @classmethod - def parse(cls, desc, testnet=False): - sh_wpkh = None - wpkh = None - sh = None - sh_wsh = None - wsh = None - origin_fingerprint = None - origin_path = None - base_key_and_path_match = None - base_key = None - path_suffix = None - multisig_M = None - multisig_N = None - - # Check the checksum - check_split = desc.split('#') - if len(check_split) > 2: - return None - if len(check_split) == 2: - if len(check_split[1]) != 8: - return None - checksum = DescriptorChecksum(check_split[0]) - if not checksum.strip(): - return None - if checksum != check_split[1]: - return None - desc = check_split[0] - - if desc.startswith("sh(wpkh("): - sh_wpkh = True - elif desc.startswith("wpkh("): - wpkh = True - elif desc.startswith("sh(wsh("): - sh_wsh = True - elif desc.startswith("wsh("): - wsh = True - elif desc.startswith("sh("): - sh = True - - if sh or sh_wsh or wsh: - if 'multi(' not in desc: - # only multisig scripts are supported - return None - # get the list of keys only - keys = desc.split(',', 1)[1].split(')', 1)[0].split(',') - if 'sortedmulti' in desc: - keys.sort(key=lambda x: x if ']' not in x else x.split(']')[1]) - multisig_M = desc.split(',')[0].split('(')[-1] - multisig_N = len(keys) - else: - keys = [desc.split('(')[-1].split(')', 1)[0]] - - descriptors = [] - for key in keys: - origin_match = re.search(r"\[(.*)\]", key) - if origin_match: - origin = origin_match.group(1) - match = re.search(r"^([0-9a-fA-F]{8})(\/.*)", origin) - if match: - origin_fingerprint = match.group(1) - origin_path = match.group(2) - # Replace h with ' - origin_path = origin_path.replace('h', '\'') - else: - origin_fingerprint = origin - origin_path = '' - - base_key_and_path_match = re.search(r"\[.*\](\w+)([\d'\/\*]*)", key) - else: - base_key_and_path_match = re.search(r"(\w+)([\d'\/\*]*)", key) - - if base_key_and_path_match: - base_key = base_key_and_path_match.group(1) - path_suffix = base_key_and_path_match.group(2) - if path_suffix == '': - path_suffix = None - else: - if origin_match is None: - return None - - descriptors.append(cls(origin_fingerprint, origin_path, base_key, path_suffix, testnet, sh_wpkh, wpkh, sh, sh_wsh, wsh)) - if len(descriptors) == 1: - return descriptors[0] - else: - # for multisig scripts save as lists all keypaths fields - return cls( - [descriptor.origin_fingerprint for descriptor in descriptors], - [descriptor.origin_path for descriptor in descriptors], - [descriptor.base_key for descriptor in descriptors], - [descriptor.path_suffix for descriptor in descriptors], - testnet, - sh_wpkh, - wpkh, - sh, - sh_wsh, - wsh, - multisig_M, - multisig_N - ) - - def serialize(self): - descriptor_open = 'pkh(' - descriptor_close = ')' - origin = '' - path_suffix = '' - - if self.wpkh: - descriptor_open = 'wpkh(' - elif self.sh_wpkh: - descriptor_open = 'sh(wpkh(' - descriptor_close = '))' - elif self.sh or self.sh_wsh or self.wsh: - # serialize multisig descriptor is not supported yet. - return None - - if self.origin_fingerprint and self.origin_path: - origin = '[' + self.origin_fingerprint + self.origin_path + ']' - - if self.path_suffix: - path_suffix = self.path_suffix - - return AddChecksum(descriptor_open + origin + self.base_key + path_suffix + descriptor_close) + def parse(cls, s: str) -> 'PubkeyProvider': + origin = None + deriv_path = None + + if s[0] == "[": + end = s.index("]") + origin = KeyOriginInfo.from_string(s[1:end]) + s = s[end + 1:] + + pubkey = s + slash_idx = s.find("/") + if slash_idx != -1: + pubkey = s[:slash_idx] + deriv_path = s[slash_idx:] + + return cls(origin, pubkey, deriv_path) + + def to_string(self) -> str: + s = "" + if self.origin: + s += "[{}]".format(self.origin.to_string()) + s += self.pubkey + if self.deriv_path: + s += self.deriv_path + return s + + def __lt__(self, other: 'PubkeyProvider') -> bool: + return self.pubkey < other.pubkey + + +class Descriptor(object): + def __init__( + self, + pubkeys: List['PubkeyProvider'], + subdescriptor: Optional['Descriptor'], + name: str + ) -> None: + self.pubkeys = pubkeys + self.subdescriptor = subdescriptor + self.name = name + + def to_string_no_checksum(self) -> str: + return "{}({}{})".format( + self.name, + ",".join([p.to_string() for p in self.pubkeys]), + self.subdescriptor.to_string_no_checksum() if self.subdescriptor else "" + ) + + def to_string(self) -> str: + return AddChecksum(self.to_string_no_checksum()) + + +class PKHDescriptor(Descriptor): + def __init__( + self, + pubkey: 'PubkeyProvider' + ) -> None: + super().__init__([pubkey], None, "pkh") + + +class WPKHDescriptor(Descriptor): + def __init__( + self, + pubkey: 'PubkeyProvider' + ) -> None: + super().__init__([pubkey], None, "wpkh") + + +class MultisigDescriptor(Descriptor): + def __init__( + self, + pubkeys: List['PubkeyProvider'], + thresh: int, + is_sorted: bool + ) -> None: + super().__init__(pubkeys, None, "sortedmulti" if is_sorted else "multi") + self.thresh = thresh + if is_sorted: + self.pubkeys.sort() + + def to_string_no_checksum(self) -> str: + return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) + + +class SHDescriptor(Descriptor): + def __init__( + self, + subdescriptor: Optional['Descriptor'] + ) -> None: + super().__init__([], subdescriptor, "sh") + + +class WSHDescriptor(Descriptor): + def __init__( + self, + subdescriptor: Optional['Descriptor'] + ) -> None: + super().__init__([], subdescriptor, "wsh") + + +def _get_func_expr(s: str) -> Tuple[str, str]: + """ + Get the function name and then the expression inside + """ + start = s.index("(") + end = s.rindex(")") + return s[0:start], s[start + 1:end] + + +def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: + end = len(expr) + comma_idx = expr.find(",") + next_expr = "" + if comma_idx != -1: + end = comma_idx + next_expr = expr[end + 1:] + return PubkeyProvider.parse(expr[:end]), next_expr + + +class _ParseDescriptorContext(Enum): + TOP = 1 + P2SH = 2 + P2WSH = 3 + + +def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor': + func, expr = _get_func_expr(desc) + if func == "pkh": + pubkey, expr = parse_pubkey(expr) + if expr: + raise ValueError("More than one pubkey in pkh descriptor") + return PKHDescriptor(pubkey) + if func == "sortedmulti" or func == "multi": + is_sorted = func == "sortedmulti" + comma_idx = expr.index(",") + thresh = int(expr[:comma_idx]) + expr = expr[comma_idx + 1:] + pubkeys = [] + while expr: + pubkey, expr = parse_pubkey(expr) + pubkeys.append(pubkey) + if len(pubkeys) == 0 or len(pubkeys) > 16: + raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 16 keys, inclusive".format(len(pubkeys))) + elif thresh < 1: + raise ValueError("Multisig threshold cannot be {}, must be at least 1".format(thresh)) + elif thresh > len(pubkeys): + raise ValueError("Multisig threshold cannot be larger than the number of keys; threshold is {} but only {} keys specified".format(thresh, len(pubkeys))) + if ctx == _ParseDescriptorContext.TOP and len(pubkeys) > 3: + raise ValueError("Cannot have {} pubkeys in bare multisig: only at most 3 pubkeys") + return MultisigDescriptor(pubkeys, thresh, is_sorted) + if ctx != _ParseDescriptorContext.P2WSH and func == "wpkh": + pubkey, expr = parse_pubkey(expr) + if expr: + raise ValueError("More than one pubkey in pkh descriptor") + return WPKHDescriptor(pubkey) + elif ctx == _ParseDescriptorContext.P2WSH and func == "wpkh": + raise ValueError("Cannot have wpkh within wsh") + if ctx == _ParseDescriptorContext.TOP and func == "sh": + subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2SH) + return SHDescriptor(subdesc) + elif ctx != _ParseDescriptorContext.TOP and func == "sh": + raise ValueError("Cannot have sh in non-top level") + if ctx != _ParseDescriptorContext.P2WSH and func == "wsh": + subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2WSH) + return WSHDescriptor(subdesc) + elif ctx == _ParseDescriptorContext.P2WSH and func == "wsh": + raise ValueError("Cannot have wsh within wsh") + if ctx == _ParseDescriptorContext.P2SH: + raise ValueError("A function is needed within P2SH") + elif ctx == _ParseDescriptorContext.P2WSH: + raise ValueError("A function is needed within P2WSH") + raise ValueError("{} is not a valid descriptor function".format(func)) + + +def parse_descriptor(desc: str) -> 'Descriptor': + i = desc.find("#") + if i != -1: + checksum = desc[i + 1:] + desc = desc[:i] + computed = DescriptorChecksum(desc) + if computed != checksum: + raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed)) + return _parse_descriptor(desc, _ParseDescriptorContext.TOP) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 1b6a1476b..6c73aab57 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -418,11 +418,11 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc # descriptor means multisig with xpubs if descriptor: pubkeys = [] - for i in range(0, descriptor.multisig_N): - xpub = ExtendedKey.deserialize(descriptor.base_key[i]) + for p in descriptor.pubkeys: + xpub = ExtendedKey.deserialize(p.pubkey) hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) - pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=parse_path('m' + descriptor.path_suffix[i]))) - multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys) + pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) + multisig = proto.MultisigRedeemScriptType(m=descriptor.thresh, signatures=[b''] * len(descriptor.pubkeys), pubkeys=pubkeys) # redeem_script means p2sh/multisig elif redeem_script: # Get multisig object required by Trezor's get_address diff --git a/hwilib/key.py b/hwilib/key.py index 8300711df..62be8cb15 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -244,6 +244,12 @@ def get_derivation_path(self) -> str: """ return "m" + self._path_string() + def get_fingerprint_hex(self) -> str: + """ + Return the hex for just the fingerprint + """ + return binascii.hexlify(self.fingerprint).decode() + def parse_path(nstr: str) -> Sequence[int]: """ diff --git a/test/test_descriptor.py b/test/test_descriptor.py index 667e814c4..0760c9c2b 100755 --- a/test/test_descriptor.py +++ b/test/test_descriptor.py @@ -1,126 +1,109 @@ #! /usr/bin/env python3 -from hwilib.descriptor import Descriptor +from hwilib.descriptor import ( + parse_descriptor, + MultisigDescriptor, + WPKHDescriptor, + WSHDescriptor, +) import unittest class TestDescriptor(unittest.TestCase): def test_parse_descriptor_with_origin(self): - desc = Descriptor.parse("wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wpkh, True) - self.assertEqual(desc.sh_wpkh, None) - self.assertEqual(desc.origin_fingerprint, "00000001") - self.assertEqual(desc.origin_path, "/84'/1'/0'") - self.assertEqual(desc.base_key, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") - self.assertEqual(desc.path_suffix, "/0/0") - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path, "m/84'/1'/0'/0/0") + d = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'") + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_multisig_descriptor_with_origin(self): - desc = Descriptor.parse("wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wsh, True) - self.assertEqual(desc.origin_fingerprint, ["00000001", "00000002"]) - self.assertEqual(desc.origin_path, ["/48'/0'/0'/2'", "/48'/0'/0'/2'"]) - self.assertEqual(desc.base_key, ["tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B", "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"]) - self.assertEqual(desc.path_suffix, ["/0/0", "/0/0"]) - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path_base, ["m/48'/0'/0'/2'", "m/48'/0'/0'/2'"]) - self.assertEqual(desc.m_path, ["m/48'/0'/0'/2'/0/0", "m/48'/0'/0'/2'/0/0"]) + d = "wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WSHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptor, MultisigDescriptor)) + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptor.pubkeys[0].deriv_path, "/0/0") + + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptor.pubkeys[1].deriv_path, "/0/0") + + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_descriptor_without_origin(self): - desc = Descriptor.parse("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wpkh, True) - self.assertEqual(desc.sh_wpkh, None) - self.assertEqual(desc.origin_fingerprint, None) - self.assertEqual(desc.origin_path, None) - self.assertEqual(desc.base_key, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") - self.assertEqual(desc.path_suffix, "/0/0") - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path, None) + d = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin, None) + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_descriptor_with_origin_fingerprint_only(self): - desc = Descriptor.parse("wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wpkh, True) - self.assertEqual(desc.sh_wpkh, None) - self.assertEqual(desc.origin_fingerprint, "00000001") - self.assertEqual(desc.origin_path, "") - self.assertEqual(desc.base_key, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") - self.assertEqual(desc.path_suffix, "/0/0") - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path, None) + d = "wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(len(desc.pubkeys[0].origin.path), 0) + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_descriptor_with_key_at_end_with_origin(self): - desc = Descriptor.parse("wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wpkh, True) - self.assertEqual(desc.sh_wpkh, None) - self.assertEqual(desc.origin_fingerprint, "00000001") - self.assertEqual(desc.origin_path, "/84'/1'/0'/0/0") - self.assertEqual(desc.base_key, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") - self.assertEqual(desc.path_suffix, None) - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path, "m/84'/1'/0'/0/0") + d = "wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'/0/0") + self.assertEqual(desc.pubkeys[0].pubkey, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_descriptor_with_key_at_end_without_origin(self): - desc = Descriptor.parse("wpkh(0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)", True) - self.assertIsNotNone(desc) - self.assertEqual(desc.wpkh, True) - self.assertEqual(desc.sh_wpkh, None) - self.assertEqual(desc.origin_fingerprint, None) - self.assertEqual(desc.origin_path, None) - self.assertEqual(desc.base_key, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") - self.assertEqual(desc.path_suffix, None) - self.assertEqual(desc.testnet, True) - self.assertEqual(desc.m_path, None) + d = "wpkh(0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin, None) + self.assertEqual(desc.pubkeys[0].pubkey, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_empty_descriptor(self): - desc = Descriptor.parse("", True) - self.assertIsNone(desc) + self.assertRaises(ValueError, parse_descriptor, "") def test_parse_descriptor_replace_h(self): - desc = Descriptor.parse("wpkh([00000001/84h/1h/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True) + d = "wpkh([00000001/84h/1h/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) self.assertIsNotNone(desc) - self.assertEqual(desc.origin_path, "/84'/1'/0'") - - def test_serialize_descriptor_with_origin(self): - descriptor = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)#mz20k55p" - desc = Descriptor.parse(descriptor, True) - self.assertEqual(desc.serialize(), descriptor) - - def test_serialize_descriptor_without_origin(self): - descriptor = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)#ac0p4yhq" - desc = Descriptor.parse(descriptor, True) - self.assertEqual(desc.serialize(), descriptor) - - def test_serialize_descriptor_with_key_at_end_with_origin(self): - descriptor = "wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)#rh7p6vk2" - desc = Descriptor.parse(descriptor, True) - self.assertEqual(desc.serialize(), descriptor) + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'") def test_checksums(self): with self.subTest(msg='Valid checksum'): - self.assertIsNotNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy")) - self.assertIsNotNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t")) - self.assertIsNotNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))")) - self.assertIsNotNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))")) with self.subTest(msg="Empty Checksum"): - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#")) - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#")) + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#") with self.subTest(msg="Too long Checksum"): - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfyq")) - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5tq")) + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfyq") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5tq") with self.subTest(msg="Too Short Checksum"): - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf")) - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5")) + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5") with self.subTest(msg="Error in Payload"): - self.assertIsNone(Descriptor.parse("sh(multi(3,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy")) - self.assertIsNone(Descriptor.parse("sh(multi(3,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t")) + self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t") with self.subTest(msg="Error in Checksum"): - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggssrxfy")) - self.assertIsNone(Descriptor.parse("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjq09x4t")) + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggssrxfy") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjq09x4t") if __name__ == "__main__": unittest.main() From 3efd6967c5a01a05ca1ebcb4f18f489865dc510a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 13:02:47 -0400 Subject: [PATCH 214/634] Typecheck descriptor.py --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 432b7499c..3a63c9ea9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,7 +70,7 @@ jobs: - pip install mypy - pip install poetry - poetry install - script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py hwilib/key.py + script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py hwilib/key.py hwilib/descriptor.py - name: Run non-device tests only stage: test install: From 66f0d3ef2000d4171b7373320541d77e759a663d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 17:03:19 -0500 Subject: [PATCH 215/634] Have setup_environment.sh check env vars before build Instead of always building every simulator/emulator, check environment variable to see what to build in setup_environment.sh --- test/setup_environment.sh | 444 +++++++++++++++++++++----------------- 1 file changed, 248 insertions(+), 196 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index acbc6d012..f0df0698f 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -1,5 +1,48 @@ #! /usr/bin/env bash +while [[ $# -gt 0 ]]; do + case $1 in + --trezor-1) + build_trezor_1=1 + shift + ;; + --trezor-t) + build_trezor_t=1 + shift + ;; + --coldcard) + build_coldcard=1 + shift + ;; + --bitbox01) + build_bitbox01=1 + shift + ;; + --ledger) + build_ledger=1 + shift + ;; + --keepkey) + build_keepkey=1 + shift + ;; + --bitcoind) + build_bitcoind=1 + shift + ;; + --all) + build_trezor_1=1 + build_trezor_t=1 + build_coldcard=1 + build_bitbox01=1 + build_ledger=1 + build_keepkey=1 + build_bitcoind=1 + shift + ;; + esac +done + # Makes debugging easier set -x @@ -7,219 +50,228 @@ set -x 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-firmware" ]; then - git clone --recursive https://github.com/trezor/trezor-firmware.git - cd trezor-firmware - trezor_setup_needed=true -else - cd trezor-firmware - 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 +if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then + # Clone trezor-mcu if it doesn't exist, or update it if it does + trezor_setup_needed=false + if [ ! -d "trezor-firmware" ]; then + git clone --recursive https://github.com/trezor/trezor-firmware.git + cd trezor-firmware trezor_setup_needed=true + else + cd trezor-firmware + 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 -fi -# Build trezor one emulator. This is pretty fast, so rebuilding every time is ok -# But there should be some caching that makes this faster -cd legacy -export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 -if [ "$trezor_setup_needed" == true ] ; then - poetry install - poetry run script/setup -fi -poetry run script/cibuild -# Delete any emulator.img file -find . -name "emulator.img" -exec rm {} \; -cd .. - -# Build trezor t emulator. This is pretty fast, so rebuilding every time is ok -# But there should be some caching that makes this faster -cd core -if [ "$trezor_setup_needed" == true ] ; then - make vendor + if [[ -n ${build_trezor_1} ]]; then + # Build trezor one emulator. This is pretty fast, so rebuilding every time is ok + # But there should be some caching that makes this faster + cd legacy + export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 + if [ "$trezor_setup_needed" == true ] ; then + poetry install + poetry run script/setup + fi + poetry run script/cibuild + # Delete any emulator.img file + find . -name "emulator.img" -exec rm {} \; + cd .. + fi + + if [[ -n ${build_trezor_t} ]]; then + # Build trezor t emulator. This is pretty fast, so rebuilding every time is ok + # But there should be some caching that makes this faster + cd core + if [ "$trezor_setup_needed" == true ] ; then + make vendor + fi + make build_unix + # Delete any emulator.img file + rm /var/tmp/trezor.flash + cd .. + fi + cd .. fi -make build_unix -# Delete any emulator.img file -rm /var/tmp/trezor.flash -cd ../.. -# Remove nanopb to avoid interfering with keepkey -pip uninstall -y nanopb - -# 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~2 # 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 + +if [[ -n ${build_coldcard} ]]; then + # 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~2 # 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 + git am ../../data/coldcard-multisig-setup.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 ../.. fi -# Apply patch to make simulator work in linux environments -git am ../../data/coldcard-linux-sock.patch -git am ../../data/coldcard-multisig-setup.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 digital bitbox firmware if it doesn't exist, or update it if it does -dbb_setup_needed=false -if [ ! -d "mcu" ]; then - git clone --recursive https://github.com/digitalbitbox/mcu.git - cd mcu - dbb_setup_needed=true -else - cd 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 - coldcard_setup_needed=true + +if [[ -n ${build_bitbox01} ]]; then + # Clone digital bitbox firmware if it doesn't exist, or update it if it does + if [ ! -d "mcu" ]; then + git clone --recursive https://github.com/digitalbitbox/mcu.git + cd mcu + else + cd 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 + fi fi + + # Build the simulator. This is cached, but it is also fast + mkdir -p build && cd build + cmake .. -DBUILD_TYPE=simulator + make -j$(nproc) + cd ../.. fi -# Build the simulator. This is cached, but it is also fast -mkdir -p build && cd build -cmake .. -DBUILD_TYPE=simulator -make -j$(nproc) -cd ../.. - -# Clone keepkey firmware if it doesn't exist, or update it if it does -keepkey_setup_needed=false -if [ ! -d "keepkey-firmware" ]; then - git clone --recursive https://github.com/keepkey/keepkey-firmware.git - cd keepkey-firmware - keepkey_setup_needed=true -else - cd keepkey-firmware - 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 +if [[ -n ${build_keepkey} ]]; then + # Clone keepkey firmware if it doesn't exist, or update it if it does + keepkey_setup_needed=false + if [ ! -d "keepkey-firmware" ]; then + git clone --recursive https://github.com/keepkey/keepkey-firmware.git + cd keepkey-firmware keepkey_setup_needed=true + else + cd keepkey-firmware + 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 + keepkey_setup_needed=true + fi fi -fi -# Build the simulator. This is cached, but it is also fast -if [ "$keepkey_setup_needed" == true ] ; then - git clone https://github.com/nanopb/nanopb.git -b nanopb-0.3.9.4 + # Build the simulator. This is cached, but it is also fast + if [ "$keepkey_setup_needed" == true ] ; then + git clone https://github.com/nanopb/nanopb.git -b nanopb-0.3.9.4 + fi + cd nanopb/generator/proto + make + cd ../../../ + export PATH=$PATH:`pwd`/nanopb/generator + cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DPROTOC_BINARY=/usr/bin/protoc + make -j$(nproc) kkemu + # Delete any emulator.img file + find . -name "emulator.img" -exec rm {} \; + cd .. fi -cd nanopb/generator/proto -make -cd ../../../ -export PATH=$PATH:`pwd`/nanopb/generator -cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DPROTOC_BINARY=/usr/bin/protoc -make -j$(nproc) kkemu -# Delete any emulator.img file -find . -name "emulator.img" -exec rm {} \; -cd .. - -# Clone ledger simulator Speculos if it doesn't exist, or update it if it does -speculos_setup_needed=false -if [ ! -d "speculos" ]; then - git clone --recursive https://github.com/LedgerHQ/speculos.git - cd speculos - speculos_setup_needed=true -else - cd speculos - 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 - speculos_setup_needed=true + +if [[ -n ${build_ledger} ]]; then + # Clone ledger simulator Speculos if it doesn't exist, or update it if it does + if [ ! -d "speculos" ]; then + git clone --recursive https://github.com/LedgerHQ/speculos.git + cd speculos + else + cd speculos + 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 + fi fi + # Apply patch to get screen info + git am ../../data/speculos-auto-button.patch + + # Build the simulator. This is cached, but it is also fast + mkdir -p build + cmake -Bbuild -H. + make -C build/ emu launcher + cd .. fi -# Apply patch to get screen info -git am ../../data/speculos-auto-button.patch - -# Build the simulator. This is cached, but it is also fast -mkdir -p build -cmake -Bbuild -H. -make -C build/ emu launcher -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/bitcoin/bitcoin.git - 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 + +if [[ -n ${build_bitcoind} ]]; then + # 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/bitcoin/bitcoin.git + 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 -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 --with-incompatible-bdb --with-miniupnpc=no --without-gui --disable-zmq --disable-tests --disable-bench --with-libs=no --with-utils=no + # Build bitcoind. This is super slow, but it is cached so it runs fairly quickly. + if [ "$bitcoind_setup_needed" == true ] ; then + ./autogen.sh + ./configure --with-incompatible-bdb --with-miniupnpc=no --without-gui --disable-zmq --disable-tests --disable-bench --with-libs=no --with-utils=no + fi + make src/bitcoind fi -make -j$(nproc) src/bitcoind From 24d2bd9187b3be4f54665aa1718a7434d80a88fb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 17:11:12 -0500 Subject: [PATCH 216/634] Install python dependencies for some simulators during setup --- test/setup_environment.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index f0df0698f..731d90292 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -135,6 +135,7 @@ if [[ -n ${build_coldcard} ]]; then git am ../../data/coldcard-multisig-setup.patch # Build the simulator. This is cached, but it is also fast + pip install -r requirements.txt cd unix if [ "$coldcard_setup_needed" == true ] ; then make setup @@ -213,6 +214,7 @@ if [[ -n ${build_keepkey} ]]; then fi if [[ -n ${build_ledger} ]]; then + pip install construct pyelftools # Clone ledger simulator Speculos if it doesn't exist, or update it if it does if [ ! -d "speculos" ]; then git clone --recursive https://github.com/LedgerHQ/speculos.git From 3f006984e05570554a4f5fc307d5851e7f21261c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 18:01:41 -0500 Subject: [PATCH 217/634] Add mypy as a dev dependency --- poetry.lock | 305 ++++++++++++++++++++++++++++++------------------- pyproject.toml | 1 + 2 files changed, 189 insertions(+), 117 deletions(-) diff --git a/poetry.lock b/poetry.lock index 44982aa94..81f6e7b9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,38 +1,38 @@ [[package]] -category = "dev" -description = "Python graph (network) package" name = "altgraph" +version = "0.17" +description = "Python graph (network) package" +category = "dev" optional = false python-versions = "*" -version = "0.17" [[package]] -category = "dev" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" name = "autopep8" +version = "1.5.4" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +category = "dev" optional = false python-versions = "*" -version = "1.5.4" [package.dependencies] pycodestyle = ">=2.6.0" toml = "*" [[package]] -category = "main" -description = "Base58 and Base58Check implementation" name = "base58" +version = "2.0.1" +description = "Base58 and Base58Check implementation" +category = "main" optional = false python-versions = ">=3.5" -version = "2.0.1" [[package]] -category = "main" -description = "Python library for bitbox02 communication" name = "bitbox02" +version = "4.1.0" +description = "Python library for bitbox02 communication" +category = "main" optional = false python-versions = ">=3.6" -version = "4.1.0" [package.dependencies] base58 = ">=2.0.0" @@ -44,42 +44,42 @@ semver = ">=2.8.1" typing-extensions = ">=3.7.4" [[package]] -category = "main" -description = "Foreign Function Interface for Python calling C code." name = "cffi" +version = "1.14.2" +description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" -version = "1.14.2" [package.dependencies] pycparser = "*" [[package]] -category = "main" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." name = "cryptography" +version = "3.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "3.1" [package.dependencies] cffi = ">=1.8,<1.11.3 || >1.11.3" six = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] +test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] -category = "main" -description = "ECDSA cryptographic signature library (pure python)" name = "ecdsa" +version = "0.16.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.16.0" [package.dependencies] six = ">=1.9.0" @@ -89,50 +89,42 @@ gmpy = ["gmpy"] gmpy2 = ["gmpy2"] [[package]] -category = "dev" -description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" +version = "3.8.3" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.3" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - [[package]] -category = "dev" -description = "Clean single-source support for Python 3 and 2" -marker = "sys_platform == \"win32\"" name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.18.2" [[package]] -category = "main" -description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" name = "hidapi" +version = "0.9.0.post3" +description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" +category = "main" optional = false python-versions = "*" -version = "0.9.0.post3" - -[package.dependencies] -setuptools = ">=19.0" [[package]] -category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" +version = "1.7.0" +description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" [package.dependencies] zipp = ">=0.5" @@ -142,204 +134,230 @@ docs = ["sphinx", "rst.linker"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] -category = "main" -description = "Pure-python wrapper for libusb-1.0" name = "libusb1" +version = "1.8" +description = "Pure-python wrapper for libusb-1.0" +category = "main" optional = false python-versions = "*" -version = "1.8" [[package]] -category = "dev" -description = "Mach-O header analysis and editing" -marker = "sys_platform == \"darwin\"" name = "macholib" +version = "1.14" +description = "Mach-O header analysis and editing" +category = "dev" optional = false python-versions = "*" -version = "1.14" [package.dependencies] altgraph = ">=0.15" [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.6.1" [[package]] -category = "main" -description = "Implementation of Bitcoin BIP-0039" name = "mnemonic" +version = "0.19" +description = "Implementation of Bitcoin BIP-0039" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.790" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" optional = false python-versions = "*" -version = "0.19" [[package]] -category = "main" -description = "Implementation of Noise Protocol Framework" name = "noiseprotocol" +version = "0.3.1" +description = "Implementation of Noise Protocol Framework" +category = "main" optional = false python-versions = "~=3.5" -version = "0.3.1" [package.dependencies] cryptography = ">=2.8" [[package]] -category = "dev" -description = "Python PE parsing module" -marker = "sys_platform == \"win32\"" name = "pefile" +version = "2019.4.18" +description = "Python PE parsing module" +category = "dev" optional = false python-versions = "*" -version = "2019.4.18" [package.dependencies] future = "*" [[package]] -category = "main" -description = "Protocol Buffers" name = "protobuf" +version = "3.13.0" +description = "Protocol Buffers" +category = "main" optional = false python-versions = "*" -version = "3.13.0" [package.dependencies] -setuptools = "*" six = ">=1.9" [[package]] -category = "main" -description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" name = "pyaes" +version = "1.6.1" +description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" +category = "main" optional = false python-versions = "*" -version = "1.6.1" [[package]] -category = "dev" -description = "Python style guide checker" name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0" [[package]] -category = "main" -description = "C parser in Python" name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.20" [[package]] -category = "dev" -description = "passive checker of Python programs" name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.2.0" [[package]] -category = "dev" -description = "PyInstaller bundles a Python application and all its dependencies into a single package." name = "pyinstaller" +version = "4.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "dev" optional = false python-versions = "*" -version = "4.0" [package.dependencies] altgraph = "*" -macholib = ">=1.8" -pefile = ">=2017.8.1" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2020.6" -pywin32-ctypes = ">=0.2.0" -setuptools = "*" +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} [package.extras] encryption = ["tinyaes (>=1.0.0)"] hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] [[package]] -category = "dev" -description = "Community maintained hooks for PyInstaller" name = "pyinstaller-hooks-contrib" +version = "2020.7" +description = "Community maintained hooks for PyInstaller" +category = "dev" optional = false python-versions = "*" -version = "2020.7" [[package]] -category = "main" -description = "Python bindings for the Qt cross-platform application and UI framework" name = "pyside2" +version = "5.15.0" +description = "Python bindings for the Qt cross-platform application and UI framework" +category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.15.0" [package.dependencies] shiboken2 = "5.15.0" [[package]] -category = "dev" -description = "" -marker = "sys_platform == \"win32\"" name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" optional = false python-versions = "*" -version = "0.2.0" [[package]] -category = "main" -description = "Python helper for Semantic Versioning (http://semver.org/)" name = "semver" +version = "2.10.2" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10.2" [[package]] -category = "main" -description = "Python / C++ bindings helper module" name = "shiboken2" +version = "5.15.0" +description = "Python / C++ bindings helper module" +category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" -version = "5.15.0" [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" [[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = "*" -version = "0.10.1" [[package]] -category = "main" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" +name = "typed-ast" +version = "1.4.2" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = "*" + +[[package]] +name = "typing-extensions" version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" [[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.1.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -349,8 +367,9 @@ testing = ["jaraco.itertools", "func-timeout"] qt = ["pyside2"] [metadata] -content-hash = "6bee57172b9beaa2587a228e24a3c7588d2472f228b4bd845ff0cc8c99f4374f" +lock-version = "1.1" python-versions = "^3.6,<3.9" +content-hash = "2867cd5b6d378c1def9404864aedde43c3a14dcd0a7c9f5d10f5469144c27f91" [metadata.files] altgraph = [ @@ -463,6 +482,26 @@ mnemonic = [ {file = "mnemonic-0.19-py2.py3-none-any.whl", hash = "sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6"}, {file = "mnemonic-0.19.tar.gz", hash = "sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931"}, ] +mypy = [ + {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, + {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, + {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, + {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, + {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, + {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, + {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, + {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, + {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, + {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, + {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, + {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, + {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, + {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] noiseprotocol = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, ] @@ -545,6 +584,38 @@ toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] +typed-ast = [ + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, + {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, + {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, + {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, + {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, + {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, + {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, + {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, + {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, + {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, +] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, diff --git a/pyproject.toml b/pyproject.toml index 7e8db36a1..09009c53c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ pefile = {version = "^2019.4",platform = "win32"} macholib = {version = "^1.11",platform = "darwin"} autopep8 = "^1.4" flake8 = "^3.7" +mypy = "^0.790" [tool.poetry.scripts] hwi = 'hwilib.cli:main' From 29a680fe358f9dbcdb4f5f6647999d21ea517570 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 16:06:05 -0500 Subject: [PATCH 218/634] Change run_tests.py --trezor to --trezor-1 --- test/run_tests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index d1d2dbffa..8223294b7 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -19,8 +19,8 @@ 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', dest='trezor', help='Do not run Trezor test with emulator', action='store_false') -trezor_group.add_argument('--trezor', dest='trezor', help='Run Trezor test with emulator', action='store_true') +trezor_group.add_argument('--no-trezor-1', dest='trezor_1', help='Do not run Trezor test with emulator', action='store_false') +trezor_group.add_argument('--trezor-1', dest='trezor_1', help='Run Trezor test with emulator', action='store_true') trezor_t_group = parser.add_mutually_exclusive_group() trezor_t_group.add_argument('--no-trezor-t', dest='trezor_t', help='Do not run Trezor T test with emulator', action='store_false') @@ -42,7 +42,7 @@ dbb_group.add_argument('--no_bitbox', dest='bitbox', help='Do not run Digital Bitbox test with simulator', action='store_false') dbb_group.add_argument('--bitbox', dest='bitbox', help='Run Digital Bitbox test with simulator', action='store_true') -parser.add_argument('--trezor-path', dest='trezor_path', help='Path to Trezor emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') +parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh') parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') @@ -53,7 +53,7 @@ parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library') -parser.set_defaults(trezor=None, trezor_t=None, coldcard=None, keepkey=None, bitbox=None, ledger=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox=None, ledger=None) args = parser.parse_args() # Run tests @@ -70,7 +70,7 @@ if args.all: # Default all true unless overridden - args.trezor = True if args.trezor is None else args.trezor + args.trezor_1 = True if args.trezor_1 is None else args.trezor_1 args.trezor_t = True if args.trezor_t is None else args.trezor_t args.coldcard = True if args.coldcard is None else args.coldcard args.keepkey = True if args.keepkey is None else args.keepkey @@ -78,14 +78,14 @@ args.ledger = True if args.ledger is None else args.ledger else: # Default all false unless overridden - args.trezor = False if args.trezor is None else args.trezor + args.trezor_1 = False if args.trezor_1 is None else args.trezor_1 args.trezor_t = False if args.trezor_t is None else args.trezor_t args.coldcard = False if args.coldcard is None else args.coldcard args.keepkey = False if args.keepkey is None else args.keepkey args.bitbox = False if args.bitbox is None else args.bitbox args.ledger = False if args.ledger is None else args.ledger -if args.trezor or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox: +if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox: # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) @@ -93,8 +93,8 @@ success &= digitalbitbox_test_suite(args.bitbox_path, rpc, userpass, args.interface) if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface) - if success and args.trezor: - success &= trezor_test_suite(args.trezor_path, rpc, userpass, args.interface) + if success and args.trezor_1: + success &= trezor_test_suite(args.trezor_1_path, rpc, userpass, args.interface) if success and args.trezor_t: success &= trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True) if success and args.keepkey: From dd33cd9ab58e86695482083ff61585d1d580fe05 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 16:29:51 -0500 Subject: [PATCH 219/634] Change run_tests.py --bitbox to --bitbox01 --- test/run_tests.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index 8223294b7..47e456f4d 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -39,21 +39,22 @@ keepkey_group.add_argument('--keepkey', dest='keepkey', help='Run Keepkey test with emulator', action='store_true') dbb_group = parser.add_mutually_exclusive_group() -dbb_group.add_argument('--no_bitbox', dest='bitbox', help='Do not run Digital Bitbox test with simulator', action='store_false') -dbb_group.add_argument('--bitbox', dest='bitbox', help='Run Digital Bitbox test with simulator', action='store_true') +dbb_group.add_argument('--no_bitbox01', dest='bitbox01', help='Do not run Digital Bitbox test with simulator', action='store_false') +dbb_group.add_argument('--bitbox01', dest='bitbox01', help='Run Digital Bitbox test with simulator', action='store_true') parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh') parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') -parser.add_argument('--bitbox-path', dest='bitbox_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') +parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py') parser.add_argument('--all', help='Run tests on all existing simulators', default=False, action='store_true') parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library') -parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox=None, ledger=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None) + args = parser.parse_args() # Run tests @@ -74,7 +75,7 @@ args.trezor_t = True if args.trezor_t is None else args.trezor_t args.coldcard = True if args.coldcard is None else args.coldcard args.keepkey = True if args.keepkey is None else args.keepkey - args.bitbox = True if args.bitbox is None else args.bitbox + args.bitbox01 = True if args.bitbox01 is None else args.bitbox01 args.ledger = True if args.ledger is None else args.ledger else: # Default all false unless overridden @@ -82,15 +83,15 @@ args.trezor_t = False if args.trezor_t is None else args.trezor_t args.coldcard = False if args.coldcard is None else args.coldcard args.keepkey = False if args.keepkey is None else args.keepkey - args.bitbox = False if args.bitbox is None else args.bitbox + args.bitbox01 = False if args.bitbox01 is None else args.bitbox01 args.ledger = False if args.ledger is None else args.ledger -if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox: +if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox01: # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - if success and args.bitbox: - success &= digitalbitbox_test_suite(args.bitbox_path, rpc, userpass, args.interface) + if success and args.bitbox01: + success &= digitalbitbox_test_suite(args.bitbox01_path, rpc, userpass, args.interface) if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface) if success and args.trezor_1: From aa837e96c5d1bc1e2869dc0abaa9c8d26d11ddbf Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 20:13:10 -0500 Subject: [PATCH 220/634] Remove udev user fail check --- test/test_udevrules.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/test_udevrules.py b/test/test_udevrules.py index 0bcbeccf8..4cb292a3a 100755 --- a/test/test_udevrules.py +++ b/test/test_udevrules.py @@ -22,11 +22,7 @@ def tearDownClass(self): removedirs(self.INSTALLATION_FOLDER) def test_rules_file_are_copied(self): - result = process_commands(['installudevrules', '--location', self.INSTALLATION_FOLDER]) - self.assertIn('error', result) - self.assertIn('code', result) - self.assertEqual(result['error'], 'Need to be root.') - self.assertEqual(result['code'], -16) + process_commands(['installudevrules', '--location', self.INSTALLATION_FOLDER]) # Assert files wre copied for _, _, files in walk(self.INSTALLATION_FOLDER, topdown=False): for file_name in files: From 4b7fafe6e4ebecb9f038924651491f56bd4d9f92 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 21 Jan 2021 00:48:32 -0500 Subject: [PATCH 221/634] Be able to skip non-device tests --- test/run_tests.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index 47e456f4d..fa4eafad4 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -53,6 +53,8 @@ parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library') +parser.add_argument("--device-only", help="Only run device tests", action="store_true") + parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None) args = parser.parse_args() @@ -60,14 +62,15 @@ # Run tests success = True suite = unittest.TestSuite() -suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor)) -suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress)) -suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT)) -suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) -suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBIP32)) -if sys.platform.startswith("linux"): - suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) -success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() +if not args.device_only: + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBIP32)) + if sys.platform.startswith("linux"): + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) + success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() if args.all: # Default all true unless overridden From 6eb454708743e7d5f658d38dd34e70d908004554 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 21 Jan 2021 02:11:00 -0500 Subject: [PATCH 222/634] Update setup_environment.sh --- test/setup_environment.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 731d90292..b110e55f7 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -52,11 +52,9 @@ cd work if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then # Clone trezor-mcu if it doesn't exist, or update it if it does - trezor_setup_needed=false if [ ! -d "trezor-firmware" ]; then git clone --recursive https://github.com/trezor/trezor-firmware.git cd trezor-firmware - trezor_setup_needed=true else cd trezor-firmware git fetch @@ -71,19 +69,19 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then echo "Up-to-date" elif [ $LOCAL = $BASE ]; then git pull - trezor_setup_needed=true fi fi + # Remove .venv so that poetry can symlink everything correctly + rm -rf .venv/ + if [[ -n ${build_trezor_1} ]]; then # Build trezor one emulator. This is pretty fast, so rebuilding every time is ok # But there should be some caching that makes this faster + poetry install cd legacy export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 - if [ "$trezor_setup_needed" == true ] ; then - poetry install - poetry run script/setup - fi + poetry run script/setup poetry run script/cibuild # Delete any emulator.img file find . -name "emulator.img" -exec rm {} \; @@ -93,11 +91,9 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then if [[ -n ${build_trezor_t} ]]; then # Build trezor t emulator. This is pretty fast, so rebuilding every time is ok # But there should be some caching that makes this faster + poetry install cd core - if [ "$trezor_setup_needed" == true ] ; then - make vendor - fi - make build_unix + poetry run make build_unix # Delete any emulator.img file rm /var/tmp/trezor.flash cd .. @@ -135,12 +131,13 @@ if [[ -n ${build_coldcard} ]]; then git am ../../data/coldcard-multisig-setup.patch # Build the simulator. This is cached, but it is also fast + poetry run pip install -r requirements.txt pip install -r requirements.txt cd unix if [ "$coldcard_setup_needed" == true ] ; then make setup fi - make -j$(nproc) + make cd ../.. fi @@ -168,12 +165,14 @@ if [[ -n ${build_bitbox01} ]]; then # Build the simulator. This is cached, but it is also fast mkdir -p build && cd build - cmake .. -DBUILD_TYPE=simulator - make -j$(nproc) + cmake .. -DBUILD_TYPE=simulator -DCMAKE_C_FLAGS="-Wno-format-truncation" + make cd ../.. fi if [[ -n ${build_keepkey} ]]; then + poetry run pip install protobuf + pip install protobuf # Clone keepkey firmware if it doesn't exist, or update it if it does keepkey_setup_needed=false if [ ! -d "keepkey-firmware" ]; then @@ -207,14 +206,15 @@ if [[ -n ${build_keepkey} ]]; then cd ../../../ export PATH=$PATH:`pwd`/nanopb/generator cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DPROTOC_BINARY=/usr/bin/protoc - make -j$(nproc) kkemu + make # Delete any emulator.img file find . -name "emulator.img" -exec rm {} \; cd .. fi if [[ -n ${build_ledger} ]]; then - pip install construct pyelftools + poetry run pip install construct mnemonic pyelftools jsonschema + pip install construct mnemonic pyelftools jsonschema # Clone ledger simulator Speculos if it doesn't exist, or update it if it does if [ ! -d "speculos" ]; then git clone --recursive https://github.com/LedgerHQ/speculos.git From 7c24028776cf35a1d1f26f976d29cf7817237688 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 02:28:56 -0500 Subject: [PATCH 223/634] Use built in Speculos automation instead of patch --- test/data/speculos-auto-button.patch | 70 ---------------------------- test/data/speculos-automation.json | 60 ++++++++++++++++++++++++ test/setup_environment.sh | 2 - test/test_ledger.py | 4 +- 4 files changed, 63 insertions(+), 73 deletions(-) delete mode 100644 test/data/speculos-auto-button.patch create mode 100644 test/data/speculos-automation.json diff --git a/test/data/speculos-auto-button.patch b/test/data/speculos-auto-button.patch deleted file mode 100644 index 4d21bb31e..000000000 --- a/test/data/speculos-auto-button.patch +++ /dev/null @@ -1,70 +0,0 @@ -From 73d6aaf4262b083bf4b8860914d9e0edcdff3ea6 Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Thu, 12 Mar 2020 17:25:09 -0400 -Subject: [PATCH] Do button presses within seproxyhal - ---- - mcu/seproxyhal.py | 35 +++++++++++++++++++++++++++++++++++ - 1 file changed, 35 insertions(+) - -diff --git a/mcu/seproxyhal.py b/mcu/seproxyhal.py -index ffaaff5..eaf22c9 100644 ---- a/mcu/seproxyhal.py -+++ b/mcu/seproxyhal.py -@@ -1,4 +1,5 @@ - import logging -+import struct - import sys - import time - import threading -@@ -141,6 +142,7 @@ class SeProxyHal: - daemon=True) - self.ticker_thread.start() - self.usb = usb.USB(self.packet_thread.queue_packet) -+ self.seen_msg_hash = False - - def _recvall(self, size): - data = b'' -@@ -248,6 +250,39 @@ class SeProxyHal: - self.logger.debug(f"DISPLAY_STATUS {data!r}") - ret = screen.display_status(data) - self.packet_thread.queue_packet(SephTag.DISPLAY_PROCESSED_EVENT, priority=True) -+ data_type, _, x, y, = struct.unpack('bbhh', data[:6]) -+ if data_type == 7: -+ if y == 12 or y ==28: -+ self.line = data[28:].decode() -+ elif y == 26: -+ self.line += data[28:].decode() -+ -+ if y == 26 or y == 28: -+ if self.line.startswith('Address') or self.line.startswith('Message hash') or self.line.startswith('Reviewoutput') or self.line.startswith('Amount') or self.line.startswith('Fees') or self.line.startswith('Confirmtransaction'): -+ self.handle_button(2, True) -+ self.handle_button(2, False) -+ if self.line.startswith('Message hash'): -+ self.seen_msg_hash = True -+ elif self.line == 'Approve' or self.line.startswith('Accept'): -+ self.handle_button(1, True) -+ self.handle_button(2, True) -+ self.handle_button(1, False) -+ self.handle_button(2, False) -+ elif self.line == 'Signmessage': -+ if self.seen_msg_hash: -+ self.handle_button(1, True) -+ self.handle_button(2, True) -+ self.handle_button(1, False) -+ self.handle_button(2, False) -+ self.seen_msg_hash = False -+ else: -+ self.handle_button(2, True) -+ self.handle_button(2, False) -+ self.handle_button(2, True) -+ self.handle_button(2, False) -+ elif self.line == 'Cancel' or self.line == 'Reject': -+ self.handle_button(1, True) -+ self.handle_button(1, False) - - elif tag == SephTag.SCREEN_DISPLAY_RAW_STATUS: - self.logger.debug("SephTag.SCREEN_DISPLAY_RAW_STATUS") --- -2.27.0 - diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json new file mode 100644 index 000000000..1ada0d43e --- /dev/null +++ b/test/data/speculos-automation.json @@ -0,0 +1,60 @@ +{ + "version": 1, + "rules": [ + { + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if you're).*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^(Accept|Approve).*", + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^Message hash.*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ], + [ "setbool", "seen_msg_hash", true ] + ] + }, + { + "text": "message", + "conditions": [ + [ "seen_msg_hash", false ] + ], + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ], + [ "setbool", "seen_msg_hash", true ] + ] + }, + { + "text": "message", + "conditions": [ + [ "seen_msg_hash", true ] + ], + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ], + [ "setbool", "seen_msg_hash", false ] + ] + }, + { + "regexp": "^(Cancel|Reject).*", + "actions": [ + [ "button", 1, true ], + [ "button", 1, false ] + ] + } + ] +} diff --git a/test/setup_environment.sh b/test/setup_environment.sh index b110e55f7..3b8da46a2 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -235,8 +235,6 @@ if [[ -n ${build_ledger} ]]; then git pull fi fi - # Apply patch to get screen info - git am ../../data/speculos-auto-button.patch # Build the simulator. This is cached, but it is also fast mkdir -p build diff --git a/test/test_ledger.py b/test/test_ledger.py index d4281a5ff..815952123 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -25,9 +25,11 @@ def __init__(self, path): pass def start(self): + automation_path = os.path.abspath("data/speculos-automation.json") + self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator - self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stderr=self.emulator_stderr, preexec_fn=os.setsid) + self.emulator_proc = subprocess.Popen(['python3', './' + os.path.basename(self.emulator_path), '--display', 'headless', '--automation', 'file:{}'.format(automation_path), '--log-level', 'automation:DEBUG', '--log-level', 'seproxyhal:DEBUG', './apps/btc.elf'], cwd=os.path.dirname(self.emulator_path), stderr=self.emulator_stderr, preexec_fn=os.setsid) # Wait for simulator to be up while True: try: From ebe2f9d115134bd6d4be927174c2f67e68c5abba Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 17:31:43 -0500 Subject: [PATCH 224/634] Add Cirrus CI configuration --- .cirrus.yml | 206 +++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + ci/cirrus.Dockerfile | 36 ++++++++ 3 files changed, 243 insertions(+) create mode 100644 .cirrus.yml create mode 100644 ci/cirrus.Dockerfile diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 000000000..b06e07303 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,206 @@ +container: + dockerfile: ci/cirrus.Dockerfile + +env: + EMAIL: cirrus@cirrus-ci.org + +sim_cache_fpr_template: &SIM_CACHE_FPR_TEMPLATE + fingerprint_script: echo $CIRRUS_BUILD_ID $DEVICE Simulator + +device_matrix_template: &DEVICE_MATRIX_TEMPLATE + - env: + DEVICE: --trezor-1 + depends_on: + - Trezor 1 Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/trezor-firmware + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --trezor-t + depends_on: + - Trezor T Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/trezor-firmware + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --coldcard + depends_on: + - Coldcard Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/firmware + << : *SIM_CACHE_FPR_TEMPLATE + sim_install_script: + - poetry run pip install -r test/work/firmware/requirements.txt + - pip install -r test/work/firmware/requirements.txt + - env: + DEVICE: --bitbox01 + depends_on: + - Bitbox01 Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/mcu + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --ledger + depends_on: + - Ledger Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/speculos + << : *SIM_CACHE_FPR_TEMPLATE + sim_install_script: + - poetry run pip install construct pyelftools mnemonic jsonschema + - pip install construct pyelftools mnemonic jsonschema + - env: + DEVICE: --keepkey + depends_on: + - Keepkey Sim Builder + - dist_builder + - bitcoind_builder + sim_work_cache: + folder: test/work/keepkey-firmware + << : *SIM_CACHE_FPR_TEMPLATE + +sim_build_matrix_template: &SIM_BUILD_MATRIX_TEMPLATE + - env: + DEVICE: --trezor-1 + name: Trezor 1 Sim Builder + sim_work_cache: + folder: test/work/trezor-firmware + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --trezor-t + name: Trezor T Sim Builder + sim_work_cache: + folder: test/work/trezor-firmware + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --coldcard + name: Coldcard Sim Builder + sim_work_cache: + folder: test/work/firmware + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --bitbox01 + name: Bitbox01 Sim Builder + sim_work_cache: + folder: test/work/mcu + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --ledger + name: Ledger Sim Builder + sim_work_cache: + folder: test/work/speculos + << : *SIM_CACHE_FPR_TEMPLATE + - env: + DEVICE: --keepkey + name: Keepkey Sim Builder + sim_work_cache: + folder: test/work/keepkey-firmware + << : *SIM_CACHE_FPR_TEMPLATE + +bitcoind_cache_template: &BITCOIND_CACHE_TEMPLATE + bitcoind_work_cache: + folder: test/work/bitcoin + fingerprint_script: echo $CIRRUS_BUILD_ID bitcoind + +dist_cache_template: &DIST_CACHE_TEMPLATE + dist_cache: + folder: dist + fingerprint_script: echo $CIRRUS_BUILD_ID dist + +lint_task: + test_script: + - flake8 + +basic_test_task: + install_script: + - poetry install + matrix: + - name: Type Check + type_check_script: > + poetry run + mypy --implicit-reexport --strict + hwilib/base58.py + hwilib/errors.py + hwilib/serializations.py + hwilib/hwwclient.py + hwilib/devices/bitbox02.py + hwilib/key.py + hwilib/descriptor.py + - name: Non-Device Tests + test_script: cd test; poetry run ./run_tests.py; cd .. + +wine_builder_task: + container: + dockerfile: contrib/build.Dockerfile + build_script: + - contrib/build_wine.sh + - sha256sum dist/* + +bitcoind_builder_task: + << : *BITCOIND_CACHE_TEMPLATE + bitcoind_cache: + folder: test/work/bitcoin + ccache_cache: + folder: /root/.ccache + env: + BUILD_BITCOIND: 1 + build_script: cd test; ./setup_environment.sh --bitcoind; cd .. + +sim_builder_task: + matrix: + << : *SIM_BUILD_MATRIX_TEMPLATE + build_script: cd test; ./setup_environment.sh $DEVICE; cd .. + +dist_builder_task: + container: + dockerfile: contrib/build.Dockerfile + << : *DIST_CACHE_TEMPLATE + build_script: + - contrib/build_bin.sh + - contrib/build_dist.sh + - sha256sum dist/* + +dist_test_task: + matrix: + << : *DEVICE_MATRIX_TEMPLATE + << : *DIST_CACHE_TEMPLATE + << : *BITCOIND_CACHE_TEMPLATE + matrix: + - name: $DEVICE Wheel + install_script: pip install dist/*.whl + test_script: cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + - name: $DEVICE Sdist + install_script: pip install $(find dist -name "*.tar.gz" -a -not -name "*amd64*") + test_script: cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + - name: $DEVICE Bindist + install_script: poetry install + test_script: cd test; poetry run ./run_tests.py $DEVICE --interface=bindist --device-only; cd .. + on_failure: + failed_script: tail -v -n +1 *.std* + +device_test_task: + matrix: + << : *DEVICE_MATRIX_TEMPLATE + << : *BITCOIND_CACHE_TEMPLATE + matrix: + - env: + INTERFACE: library + - env: + INTERFACE: cli + - env: + INTERFACE: stdin + name: $DEVICE $INTERFACE + install_script: poetry install + test_script: cd test; poetry run ./run_tests.py $DEVICE --interface=$INTERFACE --device-only; cd .. + on_failure: + failed_script: tail -v -n +1 *.std* diff --git a/README.md b/README.md index f64e4a7aa..01ab20684 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Bitcoin Hardware Wallet Interface [![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI) +[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI) The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets. It provides a standard way for software to work with hardware wallets without needing to implement device specific drivers. diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile new file mode 100644 index 000000000..45cb463ad --- /dev/null +++ b/ci/cirrus.Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.7 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update +RUN apt-get install -y \ + build-essential \ + autotools-dev \ + automake \ + cmake \ + pkg-config \ + bsdmainutils \ + libtool \ + curl \ + git \ + ccache \ + qemu-user-static \ + libsdl2-dev \ + libsdl2-image-dev \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi \ + gcc-arm-linux-gnueabihf \ + libc6-dev-armhf-cross \ + libudev-dev \ + libusb-1.0-0-dev \ + libssl-dev \ + libevent-dev \ + libdb-dev \ + libdb++-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libboost-chrono-dev \ + libboost-test-dev \ + libboost-thread-dev \ + protobuf-compiler \ + cython3 +RUN pip install poetry flake8 From a550383aa4c2e98e030593c7a734af76d668ec2d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 13:56:36 -0500 Subject: [PATCH 225/634] Remove Travis CI configuration --- .travis.yml | 130 ---------------------------------------------------- README.md | 1 - 2 files changed, 131 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a63c9ea9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,130 +0,0 @@ -language: python -os: linux -dist: bionic -python: - - '3.6.8' -cache: - pip: true - ccache: true - directories: - - test/work -addons: - apt: - packages: - - libdb-dev - - libdb++-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 - - qemu-user-static - - gcc-arm-linux-gnueabihf - - libc6-dev-armhf-cross -before_install: - - | - wget -qO- "https://github.com/crazy-max/travis-wait-enhanced/releases/download/v1.0.0/travis-wait-enhanced_1.0.0_linux_x86_64.tar.gz" | tar -zxvf - travis-wait-enhanced - mv travis-wait-enhanced /home/travis/bin/ - travis-wait-enhanced --version -install: - - pip install pipenv pysdl2 protobuf poetry==1.0.10 construct mnemonic pyelftools - # 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 - - poetry install -after_failure: - - tail -v -n +1 *.std* -jobs: - include: - - name: lint - stage: lint - install: - - pip install flake8 - script: flake8 - - name: Type annotation checking - stage: lint - install: - - pip install mypy - - pip install poetry - - poetry install - script: mypy --implicit-reexport --strict hwilib/base58.py hwilib/errors.py hwilib/serializations.py hwilib/hwwclient.py hwilib/devices/bitbox02.py hwilib/key.py hwilib/descriptor.py - - name: Run non-device tests only - stage: test - install: - - pip install poetry - - poetry install - script: cd test; poetry run ./run_tests.py - - name: With process_commands interface - stage: test - script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=library - - name: With command line interface - stage: test - script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - - name: With stdin interface - stage: test - script: cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=stdin - - name: With wheel command line interface - stage: test - services: docker - before_script: - - docker build -t hwi-builder -f contrib/build.Dockerfile . - script: - - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_dist.sh" - - sudo chown -R `whoami`:`whoami` dist/ - - pip install dist/*.whl - - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - - name: With sdist command line interface - stage: test - services: docker - before_script: - - docker build -t hwi-builder -f contrib/build.Dockerfile . - script: - - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_dist.sh" - - sudo chown -R `whoami`:`whoami` dist/ - - pip install dist/*.tar.gz - - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=cli - - name: With linux binary distribution command line interface - stage: test - services: docker - before_script: - - docker build -t hwi-builder -f contrib/build.Dockerfile . - script: - - docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_wine.sh && contrib/build_dist.sh" - - sudo chown -R `whoami`:`whoami` dist/ - - cd test; travis-wait-enhanced --timeout=40m -- poetry run ./run_tests.py --all --interface=bindist - - cd ..; sha256sum dist/* - - name: macOS binary distribution (no tests) - stage: test - os: osx - language: generic - before_install: - install: - - brew update && brew upgrade pyenv - - brew install libusb - - cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8 - script: - - contrib/build_bin.sh - - shasum -a 256 dist/* diff --git a/README.md b/README.md index 01ab20684..9505035db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Bitcoin Hardware Wallet Interface -[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI) [![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI) The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets. From ee6148454d6ef87f4c2de2f08d63c3c2a17bd1ce Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 15:26:32 -0500 Subject: [PATCH 226/634] Mention setup_environment.sh args in test/README.md --- test/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 51a90ee56..f28936d95 100644 --- a/test/README.md +++ b/test/README.md @@ -20,10 +20,13 @@ It also tests usage with `bitcoind`. `setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, and `bitcoind`. if run in the `test/` directory, these will be built in `work/test/trezor-mcu`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively. +In order to build each simulator/emulator, you will need to use command line arguments. +These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, and `--bitcoind`. +If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built. `run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, Keepkey emulator, Digital Bitbox simulator, and bitcoind. Otherwise the paths to those will need to be specified on the command line. -test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, and `test/test_digitalbitbox.py` can be disabled. +`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, and `test/test_digitalbitbox.py` can be disabled. If you are building the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`. From 8eb064b28229424b78e8d5447eeca0d6e6dea8d2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 22 Jan 2021 16:56:16 -0500 Subject: [PATCH 227/634] Fail setup_environment.sh if any command fails --- test/setup_environment.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 3b8da46a2..0aae478c7 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -44,7 +44,7 @@ while [[ $# -gt 0 ]]; do done # Makes debugging easier -set -x +set -ex # Go into the working directory mkdir -p work @@ -73,7 +73,7 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then fi # Remove .venv so that poetry can symlink everything correctly - rm -rf .venv/ + find . -type d -name ".venv" -exec rm -rf {} + if [[ -n ${build_trezor_1} ]]; then # Build trezor one emulator. This is pretty fast, so rebuilding every time is ok @@ -95,7 +95,7 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then cd core poetry run make build_unix # Delete any emulator.img file - rm /var/tmp/trezor.flash + find . -name "trezor.flash" -exec rm {} \; cd .. fi cd .. From 9d395fb73fa8ff0fcbf2fdf7a76fbd78493a6f2d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 26 Jan 2021 18:13:18 -0500 Subject: [PATCH 228/634] Allow python 3.9 --- poetry.lock | 308 ++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 166 insertions(+), 144 deletions(-) diff --git a/poetry.lock b/poetry.lock index 81f6e7b9f..68e41d81c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,15 +20,18 @@ toml = "*" [[package]] name = "base58" -version = "2.0.1" -description = "Base58 and Base58Check implementation" +version = "2.1.0" +description = "Base58 and Base58Check implementation." category = "main" optional = false python-versions = ">=3.5" +[package.extras] +tests = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "PyHamcrest (>=2.0.2)", "coveralls", "pytest-benchmark"] + [[package]] name = "bitbox02" -version = "4.1.0" +version = "5.2.0" description = "Python library for bitbox02 communication" category = "main" optional = false @@ -36,7 +39,7 @@ python-versions = ">=3.6" [package.dependencies] base58 = ">=2.0.0" -ecdsa = ">=0.13" +ecdsa = ">=0.14" hidapi = ">=0.7.99.post21" noiseprotocol = ">=0.3" protobuf = ">=3.7" @@ -45,7 +48,7 @@ typing-extensions = ">=3.7.4" [[package]] name = "cffi" -version = "1.14.2" +version = "1.14.4" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -56,14 +59,14 @@ pycparser = "*" [[package]] name = "cryptography" -version = "3.1" +version = "3.3.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" [package.dependencies] -cffi = ">=1.8,<1.11.3 || >1.11.3" +cffi = ">=1.12" six = ">=1.4.1" [package.extras] @@ -75,7 +78,7 @@ test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz" [[package]] name = "ecdsa" -version = "0.16.0" +version = "0.16.1" description = "ECDSA cryptographic signature library (pure python)" category = "main" optional = false @@ -90,7 +93,7 @@ gmpy2 = ["gmpy2"] [[package]] name = "flake8" -version = "3.8.3" +version = "3.8.4" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false @@ -112,7 +115,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "hidapi" -version = "0.9.0.post3" +version = "0.10.1" description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" category = "main" optional = false @@ -120,22 +123,23 @@ python-versions = "*" [[package]] name = "importlib-metadata" -version = "1.7.0" +version = "3.4.0" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "libusb1" -version = "1.8" +version = "1.9.1" description = "Pure-python wrapper for libusb-1.0" category = "main" optional = false @@ -216,7 +220,7 @@ future = "*" [[package]] name = "protobuf" -version = "3.13.0" +version = "3.14.0" description = "Protocol Buffers" category = "main" optional = false @@ -259,14 +263,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyinstaller" -version = "4.0" +version = "4.2" description = "PyInstaller bundles a Python application and all its dependencies into a single package." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.dependencies] altgraph = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2020.6" @@ -278,7 +283,7 @@ hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2020.7" +version = "2020.11" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false @@ -286,14 +291,14 @@ python-versions = "*" [[package]] name = "pyside2" -version = "5.15.0" +version = "5.15.2" description = "Python bindings for the Qt cross-platform application and UI framework" category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10" [package.dependencies] -shiboken2 = "5.15.0" +shiboken2 = "5.15.2" [[package]] name = "pywin32-ctypes" @@ -305,7 +310,7 @@ python-versions = "*" [[package]] name = "semver" -version = "2.10.2" +version = "2.13.0" description = "Python helper for Semantic Versioning (http://semver.org/)" category = "main" optional = false @@ -313,11 +318,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "shiboken2" -version = "5.15.0" +version = "5.15.2" description = "Python / C++ bindings helper module" category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10" [[package]] name = "six" @@ -329,11 +334,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typed-ast" @@ -353,7 +358,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.1.0" +version = "3.4.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -361,15 +366,15 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] qt = ["pyside2"] [metadata] lock-version = "1.1" -python-versions = "^3.6,<3.9" -content-hash = "2867cd5b6d378c1def9404864aedde43c3a14dcd0a7c9f5d10f5469144c27f91" +python-versions = "^3.6,<3.10" +content-hash = "7dc8a4a78469f100b9ad15ad96bc7f12b3996fd57d3860b7549b5e2ab0c7569c" [metadata.files] altgraph = [ @@ -380,95 +385,113 @@ autopep8 = [ {file = "autopep8-1.5.4.tar.gz", hash = "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"}, ] base58 = [ - {file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"}, - {file = "base58-2.0.1.tar.gz", hash = "sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79"}, + {file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"}, + {file = "base58-2.1.0.tar.gz", hash = "sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2"}, ] bitbox02 = [ - {file = "bitbox02-4.1.0-py3-none-any.whl", hash = "sha256:1af95952d67b74c80ccc0588e0aee983c764960da637bd24bc41a1cb89d5e127"}, - {file = "bitbox02-4.1.0.tar.gz", hash = "sha256:73a35594162f32897dd2b1880f0cfaa42922acd1c2d7f4cf3d94b8333329c931"}, + {file = "bitbox02-5.2.0-py3-none-any.whl", hash = "sha256:1fffe76b1311ce43da34a8935dfbe497eb15edb0ca43729481265098d679f4f5"}, + {file = "bitbox02-5.2.0.tar.gz", hash = "sha256:52b0b617660601939b30c8b588c28910946448b1b6d69ca231d5e3e47a322b71"}, ] cffi = [ - {file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"}, - {file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"}, - {file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"}, - {file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"}, - {file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"}, - {file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"}, - {file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"}, - {file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"}, - {file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"}, - {file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"}, - {file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"}, - {file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"}, - {file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"}, - {file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"}, - {file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"}, - {file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"}, + {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, + {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, + {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, + {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, + {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, + {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, + {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, + {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, + {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, + {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"}, + {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, + {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, + {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"}, + {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, + {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, + {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, + {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, + {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, + {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, ] cryptography = [ - {file = "cryptography-3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f"}, - {file = "cryptography-3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0"}, - {file = "cryptography-3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36"}, - {file = "cryptography-3.1-cp27-cp27m-win32.whl", hash = "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a"}, - {file = "cryptography-3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791"}, - {file = "cryptography-3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761"}, - {file = "cryptography-3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e"}, - {file = "cryptography-3.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8"}, - {file = "cryptography-3.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c"}, - {file = "cryptography-3.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f"}, - {file = "cryptography-3.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237"}, - {file = "cryptography-3.1-cp35-cp35m-win32.whl", hash = "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716"}, - {file = "cryptography-3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695"}, - {file = "cryptography-3.1-cp36-abi3-win32.whl", hash = "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af"}, - {file = "cryptography-3.1-cp36-abi3-win_amd64.whl", hash = "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618"}, - {file = "cryptography-3.1-cp36-cp36m-win32.whl", hash = "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1"}, - {file = "cryptography-3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c"}, - {file = "cryptography-3.1-cp37-cp37m-win32.whl", hash = "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32"}, - {file = "cryptography-3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed"}, - {file = "cryptography-3.1-cp38-cp38-win32.whl", hash = "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"}, - {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, - {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, + {file = "cryptography-3.3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812"}, + {file = "cryptography-3.3.1-cp27-cp27m-win32.whl", hash = "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e"}, + {file = "cryptography-3.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5"}, + {file = "cryptography-3.3.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c"}, + {file = "cryptography-3.3.1-cp36-abi3-win32.whl", hash = "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a"}, + {file = "cryptography-3.3.1-cp36-abi3-win_amd64.whl", hash = "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7"}, + {file = "cryptography-3.3.1.tar.gz", hash = "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6"}, ] ecdsa = [ - {file = "ecdsa-0.16.0-py2.py3-none-any.whl", hash = "sha256:ca359c971594dceebf334f3d623dae43163ab161c7d09f28cae70a86df26eb7a"}, - {file = "ecdsa-0.16.0.tar.gz", hash = "sha256:494c6a853e9ed2e9be33d160b41d47afc50a6629b993d2b9c5ad7bb226add892"}, + {file = "ecdsa-0.16.1-py2.py3-none-any.whl", hash = "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747"}, + {file = "ecdsa-0.16.1.tar.gz", hash = "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff"}, ] flake8 = [ - {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, - {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] hidapi = [ - {file = "hidapi-0.9.0.post3-cp35-cp35m-win32.whl", hash = "sha256:98bada9a2625a90a452b17b237a342c29142677c77dd0ba96072f45b0e55d5ec"}, - {file = "hidapi-0.9.0.post3-cp35-cp35m-win_amd64.whl", hash = "sha256:82d6276337d7cc25acda8b5fa99e0db497090c369611eefa18ea69c9afe55ed7"}, - {file = "hidapi-0.9.0.post3-cp36-cp36m-win32.whl", hash = "sha256:92995887078d8e7b768a60b597d1117b1aba0a5184538b633be7192daeba34cc"}, - {file = "hidapi-0.9.0.post3-cp36-cp36m-win_amd64.whl", hash = "sha256:4ee5bf9f2ece8ac73ef01f0a56ea6f62dcf024ba3beba6b29d3d52d96112931e"}, - {file = "hidapi-0.9.0.post3-cp37-cp37m-win32.whl", hash = "sha256:12288a950d7c7c3756f25405b74eb17ad84032c06a65bbbe78adea8dd247f4c0"}, - {file = "hidapi-0.9.0.post3-cp37-cp37m-win_amd64.whl", hash = "sha256:3910117ee13f3730f6810cf4b591f84dc4b55258163cbbdf9135b55deced1775"}, - {file = "hidapi-0.9.0.post3-cp38-cp38-win32.whl", hash = "sha256:a1bf3893353f654613fecc10259097d417e76ff8799f3be459aed7d1e9cee7fd"}, - {file = "hidapi-0.9.0.post3-cp38-cp38-win_amd64.whl", hash = "sha256:f70e0609c36605d3c06a91fbccc058e255918af2c59872648fe551360ad68df5"}, - {file = "hidapi-0.9.0.post3.tar.gz", hash = "sha256:5a2442928f17ba742d9c53073f48b152051c5747d758d2fefd937543da5ab2e5"}, + {file = "hidapi-0.10.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:4a081c3775a7ac850743dd345a1a4fb1175af3954c4c7a1f7508ab645f72dcb5"}, + {file = "hidapi-0.10.1-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:9ac04c6dc3d792d92b1d6ff461511853fa166a0e22f4475fe60ad647555d1caf"}, + {file = "hidapi-0.10.1-cp35-cp35m-win32.whl", hash = "sha256:310c53aa81697bf16b5f0c127afda36e5e9ea37794147afe1461422623263ef7"}, + {file = "hidapi-0.10.1-cp35-cp35m-win_amd64.whl", hash = "sha256:59f5205928dbe92513038c50dfb4f939395f8f781e176259a40f37d7a291313f"}, + {file = "hidapi-0.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5cc5f15b1b68bcb04fa290abc87070d57a8c0d2cb3a01bafeba6a7df52cd8641"}, + {file = "hidapi-0.10.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:83d8aab01afd397a0fee0017df4397fff96bef639d6176f94b747305324732aa"}, + {file = "hidapi-0.10.1-cp36-cp36m-win32.whl", hash = "sha256:0c92b398f6907654b07f7dbd7e06661abe9ad6119b403eb5fd3c2af4ce66a3b7"}, + {file = "hidapi-0.10.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4bab0e8ab066527e09856a6a345e2e0c10061f2640e9281323da9a04b94bdec1"}, + {file = "hidapi-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0f558430c086e9e0028022f4fdfaed5044e6af50cb4f12b79c498da59fc84d51"}, + {file = "hidapi-0.10.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:1e25317ac577e837154f90bc9b6da0451242211beea7a34b9bb117eec17c2e26"}, + {file = "hidapi-0.10.1-cp37-cp37m-win32.whl", hash = "sha256:b1becc9f09c85c473e91cf869b592d5d87fb8b89672988de33776b20b4c53ce1"}, + {file = "hidapi-0.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b686b2b547890c8ed17ebeabded0050ce377180a56daefa20822b4d66d3a5dea"}, + {file = "hidapi-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:39191d3e34e9a79e3dbb37898ab2ca4b84495e0815801cf84d185936e97bc6ee"}, + {file = "hidapi-0.10.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a1f4c54d71f748ee6c9ee86565ca0eba2ab9c5d88f9adacc1fe7c3b09a3f299"}, + {file = "hidapi-0.10.1-cp38-cp38-win32.whl", hash = "sha256:3b93d3f9bae38a3459491194ba1abf5c292b59dbd8738c3ac66f01b593cf3724"}, + {file = "hidapi-0.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d"}, + {file = "hidapi-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:74e968631537f52579c19c2b93e428b634dc385eb7808071bd9ff759d837fb39"}, + {file = "hidapi-0.10.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:46de7d852e10a83187da6af88d6e2cd51ba5ab336bf2ce1659bc879141570de3"}, + {file = "hidapi-0.10.1-cp39-cp39-win32.whl", hash = "sha256:095798ae1b3d6892fb0eb7ba1ab06054f6fafe6d09bc3714d80fdbf227c98f87"}, + {file = "hidapi-0.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:df4a23cd03f00d5cdc603252650df82cdd1923ceef6811cb029cc9d11a9a7a61"}, + {file = "hidapi-0.10.1.tar.gz", hash = "sha256:a1170b18050bc57fae3840a51084e8252fd319c0fc6043d68c8501deb0e25846"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, + {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, + {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, ] libusb1 = [ - {file = "libusb1-1.8.tar.gz", hash = "sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6"}, + {file = "libusb1-1.9.1-py2-none-any.whl", hash = "sha256:4a024fffe195c49f3e7eadd2266087b4be065982f0cb41ef4b7e2c5053e7e65c"}, + {file = "libusb1-1.9.1-py2-none-win32.whl", hash = "sha256:16203d77a1f623b6f8f4e6c9d6bac79c1293b8d3e11de5f2f3c30d91380ae478"}, + {file = "libusb1-1.9.1-py2-none-win_amd64.whl", hash = "sha256:3905e907156f0a3fade75ddf82a777a6a901b245aa14500429275d221a1606c2"}, + {file = "libusb1-1.9.1-py3-none-any.whl", hash = "sha256:46708965226154681f8e0b14c48325c6d02e253c218e5d3aeff846ec274ceda8"}, + {file = "libusb1-1.9.1-py3-none-win32.whl", hash = "sha256:3a53d94add2799eaa1b412e7a5e384486c9109745217b9ac7f94101ad0f41b96"}, + {file = "libusb1-1.9.1-py3-none-win_amd64.whl", hash = "sha256:b12666e8ad4df78e8f1bae36298c7d6f8f45d70ceea058b88631ef8478fd1eb0"}, + {file = "libusb1-1.9.1.tar.gz", hash = "sha256:d03ef15248c8b8ce440f6be4248eaadc074fc2dc5edd36c48e6e78eef3999292"}, ] macholib = [ {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, @@ -504,29 +527,30 @@ mypy-extensions = [ ] noiseprotocol = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, + {file = "noiseprotocol-0.3.1.tar.gz", hash = "sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645"}, ] pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, ] protobuf = [ - {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, - {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, - {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, - {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, - {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, - {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, - {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, - {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, - {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, - {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, - {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, - {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, - {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, - {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, - {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, - {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, - {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, - {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, + {file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"}, + {file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"}, + {file = "protobuf-3.14.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472"}, + {file = "protobuf-3.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142"}, + {file = "protobuf-3.14.0-cp35-cp35m-win32.whl", hash = "sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2"}, + {file = "protobuf-3.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980"}, + {file = "protobuf-3.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2"}, + {file = "protobuf-3.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1"}, + {file = "protobuf-3.14.0-cp36-cp36m-win32.whl", hash = "sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e"}, + {file = "protobuf-3.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836"}, + {file = "protobuf-3.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd"}, + {file = "protobuf-3.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac"}, + {file = "protobuf-3.14.0-cp37-cp37m-win32.whl", hash = "sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d"}, + {file = "protobuf-3.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5"}, + {file = "protobuf-3.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043"}, + {file = "protobuf-3.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00"}, + {file = "protobuf-3.14.0-py2.py3-none-any.whl", hash = "sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c"}, + {file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"}, ] pyaes = [ {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, @@ -544,45 +568,43 @@ pyflakes = [ {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pyinstaller = [ - {file = "pyinstaller-4.0.tar.gz", hash = "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0"}, + {file = "pyinstaller-4.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"}, ] pyinstaller-hooks-contrib = [ - {file = "pyinstaller-hooks-contrib-2020.7.tar.gz", hash = "sha256:74936d044f319cd7a9dca322b46a818fcb6e2af1c67af62e8a6a3121eb2863d2"}, - {file = "pyinstaller_hooks_contrib-2020.7-py2.py3-none-any.whl", hash = "sha256:5b6e06ba6072499189f5b8e1623d5f0414962941aac370ee4f842de25455be5b"}, + {file = "pyinstaller-hooks-contrib-2020.11.tar.gz", hash = "sha256:fc3290a2ca337d1d58c579c223201360bfe74caed6454eaf5a2550b77dbda45c"}, + {file = "pyinstaller_hooks_contrib-2020.11-py2.py3-none-any.whl", hash = "sha256:fa8280b79d8a2b267a2e43ff44f73b3e4a68fc8d205b8d34e8e06c960f7c2fcf"}, ] pyside2 = [ - {file = "PySide2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:ae8158d611a410c58091aa8baf24005894b4e3f40c63ff2482149481ad5395b4"}, - {file = "PySide2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:de0220cc01a8bfdaa8ccd0fc934a1ead2aedca62b49b5fd4bdcdaba6f4585a03"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:1478ea8a1ab5d8bc021ce41211933fbc238338fe70c02f7bcc2e80ea900dbf9e"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:f9099e49fb2d3571f5a81eb9ff281ce832ce8c333052e8175e2356b9c3e4a882"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:7c91a5074f3c60bac7e9336943a1dc9d5c8be8ab88a232dc55018e555dae81b2"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:2d72150f63025b9b55097c1a64d09da37ff9191f73f69237500dec7a4a130541"}, + {file = "PySide2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e"}, + {file = "PySide2-5.15.2-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0558ced3bcd7f9da638fa8b7709dba5dae82a38728e481aac8b9058ea22fcdd9"}, + {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:976cacf01ef3b397a680f9228af7d3d6273b9254457ad4204731507c1f9e6c3c"}, + {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-manylinux1_x86_64.whl", hash = "sha256:081d8c8a6c65fb1392856a547814c0c014e25ac04b38b987d9a3483e879e9634"}, + {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75"}, + {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] semver = [ - {file = "semver-2.10.2-py2.py3-none-any.whl", hash = "sha256:21e80ca738975ed513cba859db0a0d2faca2380aef1962f48272ebf9a8a44bd4"}, - {file = "semver-2.10.2.tar.gz", hash = "sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276"}, + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] shiboken2 = [ - {file = "shiboken2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:0826ce788fe55bce19a8f8a2c33d720a6ba8f59e1aab1fa9d7a53eceed3f3af5"}, - {file = "shiboken2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a92c55363d5cd3cfdd6cd28dcf91e81a00a3aa5bb177d712817c09d26bd760db"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:41a9157fb9cc7e0c0747926b25c23c3f94d59d61736a6ff763ebc7acf6afc5cf"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:5702e77ad5999ac45498c3cd47f5d078ce7406cf8dc8df74337b0cdc084bf762"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:4b0904e0967356a36e80cde05981faa14c120141856d973ee983eac0b83633c0"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:19d5f715e5ae8a815a7f148a8614a3225dceee6fd9d5decaa7749657f0f7ccbe"}, - {file = "shiboken2-5.15.0-5.15.0_1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:94991848e9ff4d03c2d7feab484113b5b5ad7f9fdfa0b0ff46ce18da47b36b58"}, - {file = "shiboken2-5.15.0-5.15.0_2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:e753324a78cbdab1c5917b5600c708a8db7e1336579e7afa20ed90edda15eefa"}, + {file = "shiboken2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c"}, + {file = "shiboken2-5.15.2-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ae8ca41274cfa057106268b6249674ca669c5b21009ec49b16d77665ab9619ed"}, + {file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:edc12a4df2b5be7ca1e762ab94e331ba9e2fbfe3932c20378d8aa3f73f90e0af"}, + {file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-abi3-manylinux1_x86_64.whl", hash = "sha256:4aee1b91e339578f9831e824ce2a1ec3ba3a463f41fda8946b4547c7eb3cba86"}, + {file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:89c157a0e2271909330e1655892e7039249f7b79a64a443d52c512337065cde0"}, + {file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:14a33169cf1bd919e4c4c4408fffbcd424c919a3f702df412b8d72b694e4c1d5"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typed-ast = [ {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, @@ -622,6 +644,6 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, ] diff --git a/pyproject.toml b/pyproject.toml index 09009c53c..b2354fe4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.6,<3.9" +python = "^3.6,<3.10" hidapi = "~0" ecdsa = "~0" pyaes = "^1.6" From 225d10e75ab8fa2f5e95e68b7997c1526f5aee5a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 26 Jan 2021 18:14:17 -0500 Subject: [PATCH 229/634] Set version to 2.0.0-dev --- hwilib/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 109 ++------------------------------------------- 3 files changed, 6 insertions(+), 107 deletions(-) diff --git a/hwilib/__init__.py b/hwilib/__init__.py index 7b344eca4..b553f4d61 100644 --- a/hwilib/__init__.py +++ b/hwilib/__init__.py @@ -1 +1 @@ -__version__ = '1.1.2' +__version__ = '2.0.0-dev' diff --git a/pyproject.toml b/pyproject.toml index b2354fe4e..a2d04ce1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hwi" -version = "1.1.2" +version = "2.0.0-dev" description = "A library for working with Bitcoin hardware wallets" authors = ["Andrew Chow "] license = "MIT" diff --git a/setup.py b/setup.py index cd10f7806..5036fd780 100644 --- a/setup.py +++ b/setup.py @@ -11,108 +11,7 @@ 'hwilib.devices.trezorlib.transport'] package_data = \ -{'': ['*'], - 'hwilib': ['udev/*', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/bitbox02pairing.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/displayaddressdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getkeypooloptionsdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/getxpubdialog.ui', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/hwiqt.pyproject', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/mainwindow.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/sendpindialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/setpassphrasedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signmessagedialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui', - 'ui/signpsbtdialog.ui']} +{'': ['*'], 'hwilib': ['udev/*', 'ui/*']} modules = \ ['hwi', 'hwi-qt'] @@ -133,9 +32,9 @@ setup_kwargs = { 'name': 'hwi', - 'version': '1.1.2', + 'version': '2.0.0.dev0', 'description': 'A library for working with Bitcoin hardware wallets', - 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", + 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependecies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |\n| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |\n| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Andrew Chow', 'author_email': 'andrew@achow101.com', 'maintainer': None, @@ -147,7 +46,7 @@ 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, - 'python_requires': '>=3.6,<3.9', + 'python_requires': '>=3.6,<3.10', } From ce2bb9972065da95bfa31b10473013918c31ca61 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 18:37:52 -0500 Subject: [PATCH 230/634] Add get_full_derivation_path() to PubkeyProvider get_full_derivation_path() retrieves the derivation path of the public key at the given position. This includes the entire key origin as well as any additional derivation. --- hwilib/descriptor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 5d56bd86f..261429bc7 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -95,6 +95,16 @@ def to_string(self) -> str: s += self.deriv_path return s + def get_full_derivation_path(self, pos: int) -> str: + """ + Returns the full derivation path at the given position, including the origin + """ + path = self.origin.get_derivation_path() if self.origin is not None else "m/" + path += self.deriv_path if self.deriv_path is not None else "" + if path[-1] == "*": + path = path[:-1] + str(pos) + return path + def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey From 60b94cd25c0de4130dba84f9aacd2e69a67a1c0c Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 18:38:31 -0500 Subject: [PATCH 231/634] Use full derivation path for descriptor displayaddress Display address was accidentally changed to only use the origin derivation path. It should be the full derivation path. --- hwilib/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 54b41150f..904d2e77c 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -301,7 +301,7 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path())['xpub'] if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} - return client.display_address(pubkey.origin.get_derivation_path(), is_sh and is_wpkh, not is_sh and is_wpkh) + return client.display_address(pubkey.get_full_derivation_path(0), is_sh and is_wpkh, not is_sh and is_wpkh) def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) From b19a3dd546ea3c4e01f9699f0e3fd37ef1c9878b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 18:39:19 -0500 Subject: [PATCH 232/634] Use subtests in signtx It's easier to debug with these tests to be subtests --- test/test_device.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 131aec95d..612cc8307 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -409,10 +409,15 @@ def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'} supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} - self._test_signtx("legacy", self.full_type in supports_multisig, self.full_type in supports_external) - self._test_signtx("segwit", self.full_type in supports_multisig, self.full_type in supports_external) + multisig = self.full_type in supports_multisig + external = self.full_type in supports_external + with self.subTest(addrtype="legacy", multisig=multisig, external=external): + self._test_signtx("legacy", multisig, external) + with self.subTest(addrtype="segwit", multisig=multisig, external=external): + self._test_signtx("segwit", multisig, external) if self.full_type in supports_mixed: - self._test_signtx("all", self.full_type in supports_multisig, self.full_type in supports_external) + with self.subTest(addrtype="all", multisig=multisig, external=external): + self._test_signtx("all", multisig, external) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): From cd74be3113ae39c6361a9bb57e9d359b3a050c44 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 18:39:47 -0500 Subject: [PATCH 233/634] Use multsig deriv paths that Trezor T likes --- test/test_device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 612cc8307..6bbd9dd85 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -325,7 +325,7 @@ def _make_multisigs(self): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): - path = "/48h/1h/{}h/0/0".format(i) + path = "/48h/1h/{}h/0h/0/0".format(i) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) @@ -526,7 +526,7 @@ def _make_single_multisig(self, addrtype): desc_pubkeys = [] sorted_pubkeys = [] for i in range(0, 3): - path = "/48h/1h/{}h/0/0".format(i) + path = "/48h/1h/{}h/0h/0/0".format(i) origin = '{}{}'.format(self.fingerprint, path) xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) @@ -583,8 +583,8 @@ def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type)) - account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h'])['xpub'] - desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h]' + account_xpub + '/1/0))' + account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h/0h'])['xpub'] + desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/1/0))' result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc]) self.assertNotIn('error', result) self.assertNotIn('code', result) From 66f5cf83a1d85b012c3c6456530890a51db7b6c2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 19:11:38 -0500 Subject: [PATCH 234/634] trezor: Use an allowed key path for ignored sigs m/84h/1h is disallowed now. Instead use a bigger path m/84h/1h/0h/0/0 --- hwilib/devices/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 6c73aab57..588e8422f 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -250,7 +250,7 @@ def sign_tx(self, tx): p2wsh = True def ignore_input(): - txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0)] + txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0), 0x80000000, 0, 0] txinputtype.multisig = None txinputtype.script_type = proto.InputScriptType.SPENDWITNESS inputs.append(txinputtype) From 973aa3701caaabfa24354dcff9daa9ab94937d9e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 19:12:51 -0500 Subject: [PATCH 235/634] tests: Use trezor t purpose 48 path Trezor has decided to impose further path restrictions on multisigs. Now they must use "purpose 48". At least it's documented: https://github.com/trezor/trezor-firmware/blob/master/docs/misc/purpose48.md This is incompatible with what we were doing previously because now the address type is part of determining the derivation path. --- test/test_device.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 6bbd9dd85..657e3c733 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -322,24 +322,32 @@ def _generate_and_finalize(self, unknown_inputs, psbt): return finalize_res['hex'] def _make_multisigs(self): - desc_pubkeys = [] - sorted_pubkeys = [] - for i in range(0, 3): - path = "/48h/1h/{}h/0h/0/0".format(i) - origin = '{}{}'.format(self.fingerprint, path) - xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) - desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) - sorted_pubkeys.append(xpub["pubkey"]) - sorted_pubkeys.sort() - + def get_pubkeys(t): + desc_pubkeys = [] + sorted_pubkeys = [] + for i in range(0, 3): + path = "/48h/1h/{}h/{}h/0/0".format(i, t) + origin = '{}{}'.format(self.fingerprint, path) + xpub = self.do_command(self.dev_args + ["--expert", "getxpub", "m{}".format(path)]) + desc_pubkeys.append("[{}]{}".format(origin, xpub["pubkey"])) + sorted_pubkeys.append(xpub["pubkey"]) + sorted_pubkeys.sort() + return desc_pubkeys, sorted_pubkeys + + desc_pubkeys, sorted_pubkeys = get_pubkeys(0) sh_desc = AddChecksum("sh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[0], desc_pubkeys[1], desc_pubkeys[2])) sh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "legacy") self.assertEqual(self.rpc.deriveaddresses(sh_desc)[0], sh_ms_info["address"]) + # Trezor requires that each address type uses a different derivation path. + # Other devices don't have this requirement, and in the tests involving multiple address types, Coldcard will fail. + # So for those other devices, stick to the 0 path. + desc_pubkeys, sorted_pubkeys = get_pubkeys(1) if self.full_type == "trezor_t" else get_pubkeys(0) sh_wsh_desc = AddChecksum("sh(wsh(sortedmulti(2,{},{},{})))".format(desc_pubkeys[1], desc_pubkeys[2], desc_pubkeys[0])) sh_wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "p2sh-segwit") self.assertEqual(self.rpc.deriveaddresses(sh_wsh_desc)[0], sh_wsh_ms_info["address"]) + desc_pubkeys, sorted_pubkeys = get_pubkeys(2) if self.full_type == "trezor_t" else get_pubkeys(0) wsh_desc = AddChecksum("wsh(sortedmulti(2,{},{},{}))".format(desc_pubkeys[2], desc_pubkeys[1], desc_pubkeys[0])) wsh_ms_info = self.rpc.createmultisig(2, sorted_pubkeys, "bech32") self.assertEqual(self.rpc.deriveaddresses(wsh_desc)[0], wsh_ms_info["address"]) From d491dffad4706a7eb34ec3642ade5ad05768317a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 22:01:46 -0500 Subject: [PATCH 236/634] Patch Coldcard simulator to work with our multisig tests --- test/data/coldcard-linux-sock.patch | 30 ------ test/data/coldcard-multisig-setup.patch | 29 ----- test/data/coldcard-multisig.patch | 137 ++++++++++++++++++++++++ test/setup_environment.sh | 3 +- 4 files changed, 138 insertions(+), 61 deletions(-) delete mode 100644 test/data/coldcard-linux-sock.patch delete mode 100644 test/data/coldcard-multisig-setup.patch create mode 100644 test/data/coldcard-multisig.patch diff --git a/test/data/coldcard-linux-sock.patch b/test/data/coldcard-linux-sock.patch deleted file mode 100644 index 585fbd797..000000000 --- a/test/data/coldcard-linux-sock.patch +++ /dev/null @@ -1,30 +0,0 @@ -From b0513638e494afa6414a73e2b56f282aba8325cb Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Tue, 27 Nov 2018 17:32:44 -0500 -Subject: [PATCH 1/2] 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 0fd96de..3ac83f6 100644 ---- a/unix/frozen-modules/pyb.py -+++ b/unix/frozen-modules/pyb.py -@@ -25,10 +25,10 @@ class USB_HID: - fn = b'/tmp/ckcc-simulator.sock' - self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - # If on linux, try commenting the following line -- 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.27.0 - diff --git a/test/data/coldcard-multisig-setup.patch b/test/data/coldcard-multisig-setup.patch deleted file mode 100644 index 4a45ec549..000000000 --- a/test/data/coldcard-multisig-setup.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 8793fbaa9b32f3c67f289a05194e68acc5c61b7d Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Tue, 17 Dec 2019 17:56:05 -0500 -Subject: [PATCH 2/2] Change default simulator multisig - ---- - unix/frozen-modules/sim_settings.py | 6 +++++- - 1 file changed, 5 insertions(+), 1 deletion(-) - -diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py -index 0313c3e..6d301d4 100644 ---- a/unix/frozen-modules/sim_settings.py -+++ b/unix/frozen-modules/sim_settings.py -@@ -68,7 +68,11 @@ if '--ms' in sys.argv: - sim_defaults['multisig'] = [["CC-2-of-4", [2, 4], [[1130956047, "tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP"], [3503269483, "tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax"], [2389277556, "tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM"], [3190206587, "tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt"]], {"pp": "48'/1'/0'/1'", "ch": "XTN", "ft": 26}]] - else: - # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator -- sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] -+ sim_defaults['multisig'] = [ -+ ['mstest', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 8, 'ch': 'XTN'}], -+ ['mstest1', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 14, 'ch': 'XTN'}], -+ ['mstest2', [2, 3], [[1130956047, 'tpubDCp1a2CuSdeVLYbjKRF6H1oSU2hubMm6oV4tXYFkh5u7BwhJ1P5ZwntWGfNCx92BUWpbYPcbgApbYHNTEED49vFdscWgg6KpJepKdgBB92U'], [1130956047, 'tpubDCp1a2CuSdeVQxVNMuLWW4GDnSYqzaPwYRfb8uveDQmwYaPBXERNPWUR8GvyfSLDsbr9MwQxLsKeAAjSsigKpmgrkLdHJM77C3us7t5QFL2'], [1130956047, 'tpubDCp1a2CuSdeVS6h1LsXGqpALo6ZhvWEFDYDv8qTgcnBZYdAPsZ7QL25UKdUqKsMcc8eMQyZA9zjvUMaJjdSVcfDftzgmdvJfH5MnrZoxzFG']], {'ft': 26, 'ch': 'XTN'}], -+ ] - sim_defaults['fee_limit'] = -1 - - if '--xfp' in sys.argv: --- -2.28.0 - diff --git a/test/data/coldcard-multisig.patch b/test/data/coldcard-multisig.patch new file mode 100644 index 000000000..e138f4eb4 --- /dev/null +++ b/test/data/coldcard-multisig.patch @@ -0,0 +1,137 @@ +From f91b0cf38f640a031de08e27081599da13be92fe Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Tue, 27 Nov 2018 17:32:44 -0500 +Subject: [PATCH 1/3] 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 5341be7..b9d32d0 100644 +--- a/unix/frozen-modules/pyb.py ++++ b/unix/frozen-modules/pyb.py +@@ -26,10 +26,10 @@ class USB_HID: + fn = b'/tmp/ckcc-simulator.sock' + self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + # If on linux, try commenting the following line +- 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.30.0 + + +From 32d4235a79e8cde7ee1c9901e0c834c310a29725 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Tue, 17 Dec 2019 17:56:05 -0500 +Subject: [PATCH 2/3] Change default simulator multisig + +--- + unix/frozen-modules/sim_settings.py | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/unix/frozen-modules/sim_settings.py b/unix/frozen-modules/sim_settings.py +index 650f255..cf5bbd4 100644 +--- a/unix/frozen-modules/sim_settings.py ++++ b/unix/frozen-modules/sim_settings.py +@@ -68,7 +68,11 @@ if '--ms' in sys.argv: + sim_defaults['multisig'] = [["CC-2-of-4", [2, 4], [[1130956047, "tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP"], [3503269483, "tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax"], [2389277556, "tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM"], [3190206587, "tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt"]], {"pp": "48'/1'/0'/1'", "ch": "XTN", "ft": 26}]] + else: + # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator +- sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] ++ sim_defaults['multisig'] = [ ++ ['mstest', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrR9x68P5Jm8WjhCE4atyGiPviFA9ve5iMnYbkTjof2HjzejcQcD7getPusDLPsWJLN2UttzK3pyVgBkRs52MiRZM7ZJ8TrEq'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 8, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], ++ ['mstest1', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 14, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], ++ ['mstest2', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 26, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], ++ ] + sim_defaults['fee_limit'] = -1 + + if '--xfp' in sys.argv: +-- +2.30.0 + + +From 8cf7883d84ca5e30638b4aff0cd12c7e9a01e643 Mon Sep 17 00:00:00 2001 +From: Andrew Chow +Date: Wed, 27 Jan 2021 21:50:22 -0500 +Subject: [PATCH 3/3] Allow multisigs to share master fingerprint + +--- + shared/multisig.py | 38 ++++++++++++++++++++++++-------------- + 1 file changed, 24 insertions(+), 14 deletions(-) + +diff --git a/shared/multisig.py b/shared/multisig.py +index b7e4f44..e3b5807 100644 +--- a/shared/multisig.py ++++ b/shared/multisig.py +@@ -141,9 +141,9 @@ class MultisigWallet: + # calc useful cache value: numeric xfp+subpath, with lookup + self.xfp_paths = {} + for xfp, deriv, _ in self.xpubs: +- self.xfp_paths[xfp] = str_to_keypath(xfp, deriv) ++ self.xfp_paths.setdefault(xfp, list()).append(str_to_keypath(xfp, deriv)) + +- assert len(self.xfp_paths) == self.N, 'dup XFP' # not supported ++ assert len(self.xpubs) == self.N, 'Number of pubkeys does not match N' + + @classmethod + def render_addr_fmt(cls, addr_fmt): +@@ -244,7 +244,11 @@ class MultisigWallet: + + def get_xfp_paths(self): + # return list of lists [xfp, *deriv] +- return list(self.xfp_paths.values()) ++ ret = [] ++ for paths_list in self.xfp_paths: ++ for xfp_path in paths_list: ++ ret.append(xfp_path) ++ return ret + + @classmethod + def find_match(cls, M, N, xfp_paths, addr_fmt=None): +@@ -282,17 +286,23 @@ class MultisigWallet: + for x in xfp_paths: + if x[0] not in self.xfp_paths: + return False +- prefix = self.xfp_paths[x[0]] +- +- if len(x) < len(prefix): +- # PSBT specs a path shorter than wallet's xpub +- #print('path len: %d vs %d' % (len(prefix), len(x))) +- return False +- +- comm = len(prefix) +- if tuple(prefix[:comm]) != tuple(x[:comm]): +- # xfp => maps to wrong path +- #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) ++ for prefix in self.xfp_paths[x[0]]: ++ if len(x) < len(prefix): ++ # PSBT specs a path shorter than wallet's xpub ++ #print('path len: %d vs %d' % (len(prefix), len(x))) ++ return False ++ ++ comm = len(prefix) ++ if tuple(prefix[:comm]) != tuple(x[:comm]): ++ # xfp => maps to wrong path ++ # But maybe there is another path that does match, so keep going ++ #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) ++ continue ++ else: ++ # Found a match, cleanly exit ++ break ++ else: ++ # No match was found + return False + + return True +-- +2.30.0 + diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 0aae478c7..cad2146ac 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -127,8 +127,7 @@ if [[ -n ${build_coldcard} ]]; then fi fi # Apply patch to make simulator work in linux environments - git am ../../data/coldcard-linux-sock.patch - git am ../../data/coldcard-multisig-setup.patch + git am ../../data/coldcard-multisig.patch # Build the simulator. This is cached, but it is also fast poetry run pip install -r requirements.txt From 33f614b445b19e7dc9b1322254470a29427a3b8a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 28 Jan 2021 00:16:12 -0500 Subject: [PATCH 237/634] Remove poetry version restriction in build_*.sh --- contrib/build.Dockerfile | 4 ++++ contrib/build_bin.sh | 2 +- contrib/build_dist.sh | 2 +- contrib/build_wine.sh | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index 3303c151c..eb92a943e 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -49,3 +49,7 @@ RUN apt-get install --install-recommends -y \ wine-stable \ winehq-stable \ p7zip-full + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index a96c72697..99e2082e2 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -6,7 +6,7 @@ set -ex eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip -pip install poetry==1.0.10 +pip install poetry # Setup poetry and install the dependencies poetry install -E qt diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index 1c7c5ac19..c8bd7fd36 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -6,7 +6,7 @@ set -ex eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" pip install -U pip -pip install poetry==1.0.10 +pip install poetry # Setup poetry and install the dependencies poetry install -E qt diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index c09a67b58..7b48b204e 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -58,7 +58,7 @@ popd $PYTHON -m pip install -U pip # Install Poetry and things needed for pyinstaller -$PYTHON -m pip install poetry==1.0.10 +$PYTHON -m pip install poetry # We also need to change the timestamps of all of the base library files lib_dir=~/.wine/drive_c/python3/Lib From 8eb801c16dc4552d0fbaab9d3048b91845d2c557 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 28 Jan 2021 01:42:41 -0500 Subject: [PATCH 238/634] Use python:3.6 docker image for Cirrus Cirrus should be testing against our oldest supported Python version. Actually ideally it should be testing against all current python versions, but we aren't there yet. --- ci/cirrus.Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile index 45cb463ad..f01ced7c6 100644 --- a/ci/cirrus.Dockerfile +++ b/ci/cirrus.Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.6 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update @@ -34,3 +34,7 @@ RUN apt-get install -y \ protobuf-compiler \ cython3 RUN pip install poetry flake8 + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 From 63c2310795fc46ebaa0398f7f2fd326d2e796a80 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 18 Nov 2020 16:18:04 +0100 Subject: [PATCH 239/634] bitbox02: update bitbox02 to 5.1.0 Updated pyproject.toml and setup.py, then: `poetry update bitbox02 --lock`. --- hwilib/devices/bitbox02.py | 3 ++- poetry.lock | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 382f52832..bdd81d6fa 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -615,7 +615,8 @@ def backup_device( "Label/passphrase not needed when exporting mnemonic from the BitBox02." ) - return {"success": self.init().show_mnemonic()} + self.init().show_mnemonic() + return {"success": True} @bitbox02_exception def restore_device( diff --git a/poetry.lock b/poetry.lock index 68e41d81c..fe1e1d896 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,7 +374,7 @@ qt = ["pyside2"] [metadata] lock-version = "1.1" python-versions = "^3.6,<3.10" -content-hash = "7dc8a4a78469f100b9ad15ad96bc7f12b3996fd57d3860b7549b5e2ab0c7569c" +content-hash = "fae8cd4ec5fae48e4eed77a42ff209233bf32fff1e7a79854a492f7c06582c4f" [metadata.files] altgraph = [ diff --git a/pyproject.toml b/pyproject.toml index a2d04ce1a..22d863d34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ mnemonic = "~0" typing-extensions = "^3.7" libusb1 = "^1.7" pyside2 = { version = "^5.14.0", optional = true } -bitbox02 = ">=4.1.0" +bitbox02 = ">=5.1.0" [tool.poetry.extras] qt = ["pyside2"] diff --git a/setup.py b/setup.py index 5036fd780..7e7d27f0c 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ modules = \ ['hwi', 'hwi-qt'] install_requires = \ -['bitbox02>=4.1.0', +['bitbox02>=5.1.0', 'ecdsa>=0,<1', 'hidapi>=0,<1', 'libusb1>=1.7,<2.0', From d8ab17413f908dc955f5621c26f93871079b2d55 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 28 Jan 2021 18:06:59 -0500 Subject: [PATCH 240/634] Remove hidapi.so strip during build_bin.sh Poetry >=1.1.0 seems to be building the cython .so files deterministically now so there is no need for us to make it deterministic by stripping them. Additionally, stripping them when poetry >=1.1.0 did the build is causing the resulting binaries to fail, so this should fix that too. --- contrib/build_bin.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 99e2082e2..00147a3bb 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -11,13 +11,6 @@ pip install poetry # Setup poetry and install the dependencies poetry install -E qt -# We now need to remove debugging symbols and build id from the hidapi SO file -so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages -strip -x ${so_dir}/hid*.so -if [[ $OSTYPE != *"darwin"* ]]; then - strip -R .note.gnu.build-id ${so_dir}/hid*.so -fi - # We also need to change the timestamps of all of the base library files lib_dir=`pyenv root`/versions/3.6.8/lib/python3.6 TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; From 5707be358b42a13a2a3e11881a3a9ecd8ecfa000 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 19 Jan 2021 14:14:47 -0500 Subject: [PATCH 241/634] Move AddressType to serializations --- hwilib/commands.py | 7 +------ hwilib/serializations.py | 7 +++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 904d2e77c..64a2a7dba 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -6,7 +6,7 @@ import importlib import platform -from .serializations import PSBT +from .serializations import AddressType, PSBT from .base58 import xpub_to_pub_hex from .key import ( H_, @@ -33,15 +33,10 @@ ) from .devices import __all__ as all_devs -from enum import Enum from itertools import count py_enumerate = enumerate -class AddressType(Enum): - PKH = 1 - WPKH = 2 - SH_WPKH = 3 # Get the client for the device def get_client(device_type, device_path, password='', expert=False): diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 4fb79ca1c..128c60466 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -24,6 +24,7 @@ import copy import base64 +from enum import Enum from io import BytesIO, BufferedReader from typing import ( Dict, @@ -63,6 +64,12 @@ def hash160(s: bytes) -> bytes: return ripemd160(sha256(s)) +class AddressType(Enum): + PKH = 1 + WPKH = 2 + SH_WPKH = 3 + + # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: r = b"" From ab402c9c74b29ce154291d13cbe07773c62d7227 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 19 Jan 2021 14:38:12 -0500 Subject: [PATCH 242/634] Change getkeypool to use --addr-type for address type Instead of individual switches for each address type, use the existing AddressType enum via a --addr-type option. --- hwilib/cli.py | 8 ++++---- hwilib/commands.py | 11 +++-------- hwilib/serializations.py | 13 +++++++++++++ test/test_device.py | 8 ++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index fd25d2a31..c7fa74a53 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -28,6 +28,7 @@ NO_DEVICE_TYPE, UNAVAILABLE_ACTION, ) +from .serializations import AddressType from . import __version__ import argparse @@ -52,7 +53,7 @@ def getxpub_handler(args, client): return getxpub(client, path=args.path) def getkeypool_handler(args, client): - return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh, addr_all=args.all) + return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, addr_type=args.addr_type, addr_all=args.all) def getdescriptors_handler(args, client): return getdescriptors(client, account=args.account) @@ -158,11 +159,10 @@ def process_commands(cli_args): kparg_group.add_argument('--nokeypool', action='store_false', dest='keypool', help='Indicates that the keys are not to be imported to the keypool', default=False) getkeypool_parser.add_argument('--internal', action='store_true', help='Indicates that the keys are change keys') kp_type_group = getkeypool_parser.add_mutually_exclusive_group() - kp_type_group.add_argument('--sh_wpkh', action='store_true', help='Generate p2sh-nested segwit addresses (default path: m/49h/0h/0h/[0,1]/*)') - kp_type_group.add_argument('--wpkh', action='store_true', help='Generate bech32 addresses (default path: m/84h/0h/0h/[0,1]/*)') + kp_type_group.add_argument("--addr-type", help="The address type (and default derivation path) to produce descriptors for", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) kp_type_group.add_argument('--all', action='store_true', help='Generate addresses for all standard address types (default paths: m/{44,49,84}h/0h/0h/[0,1]/*)') getkeypool_parser.add_argument('--account', help='BIP43 account', type=int, default=0) - getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. m/84h/0h/0h/1/* with --wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') + getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. m/84h/0h/0h/1/* with --addr-type wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') getkeypool_parser.add_argument('start', type=int, help='The index to start at.') getkeypool_parser.add_argument('end', type=int, help='The index to end at.') getkeypool_parser.set_defaults(func=getkeypool_handler) diff --git a/hwilib/commands.py b/hwilib/commands.py index 64a2a7dba..b77e4876d 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -197,16 +197,11 @@ def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, else: return PKHDescriptor(pubkey) -def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, sh_wpkh=False, wpkh=True, addr_all=False): +def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, addr_type: AddressType = AddressType.PKH, addr_all=False): - if sh_wpkh: - addr_types = [AddressType.SH_WPKH] - elif wpkh: - addr_types = [AddressType.WPKH] - elif addr_all: + addr_types = [addr_type] + if addr_all: addr_types = list(AddressType) - else: - addr_types = [AddressType.PKH] # When no specific path or internal-ness is specified, create standard types chains = [] diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 128c60466..e2a5f72ad 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -35,6 +35,7 @@ Sequence, Tuple, TypeVar, + Union, Callable, ) from typing_extensions import Protocol @@ -69,6 +70,18 @@ class AddressType(Enum): WPKH = 2 SH_WPKH = 3 + def __str__(self) -> str: + return self.name.lower() + + def __repr__(self) -> str: + return str(self) + + @staticmethod + def argparse(s: str) -> Union['AddressType', str]: + try: + return AddressType[s.upper()] + except KeyError: + return s # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: diff --git a/test/test_device.py b/test/test_device.py index 657e3c733..98ff1a3cc 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -192,7 +192,7 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('legacy')) self.assertTrue(addr_info['hdkeypath'].startswith("m/44'/1'/0'/1/")) - shwpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '0', '20']) + shwpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', "--addr-type", "sh_wpkh", '0', '20']) import_result = self.wrpc.importdescriptors(shwpkh_keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -201,7 +201,7 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/0'/1/")) - wpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '0', '20']) + wpkh_keypool_desc = self.do_command(self.dev_args + ['getkeypool', "--addr-type", "wpkh", '0', '20']) import_result = self.wrpc.importdescriptors(wpkh_keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -214,7 +214,7 @@ def test_getkeypool(self): all_keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '0', '20']) self.assertEqual(all_keypool_desc, pkh_keypool_desc + wpkh_keypool_desc + shwpkh_keypool_desc) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--sh_wpkh', '--account', '3', '0', '20']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', "--addr-type", "sh_wpkh", '--account', '3', '0', '20']) import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -222,7 +222,7 @@ def test_getkeypool(self): self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/0/")) addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertTrue(addr_info['hdkeypath'].startswith("m/49'/1'/3'/1/")) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--wpkh', '--account', '3', '0', '20']) + keypool_desc = self.do_command(self.dev_args + ['getkeypool', "--addr-type", "wpkh", '--account', '3', '0', '20']) import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): From 357b59ff2c77347ea9194a975892fee7b4f64b4d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 19 Jan 2021 14:39:19 -0500 Subject: [PATCH 243/634] Change displayaddress to use --addr-type Instead of individual switches for each address type, use the AddressType enum. --- hwilib/cli.py | 5 ++--- hwilib/commands.py | 21 +++++++++++++-------- hwilib/devices/bitbox02.py | 8 ++++---- hwilib/devices/coldcard.py | 7 ++++--- hwilib/devices/digitalbitbox.py | 3 ++- hwilib/devices/ledger.py | 7 +++++-- hwilib/devices/trezor.py | 7 ++++--- hwilib/hwwclient.py | 5 ++--- test/test_device.py | 13 ++++--------- 9 files changed, 40 insertions(+), 36 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index c7fa74a53..91b6c567d 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -41,7 +41,7 @@ def backup_device_handler(args, client): return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) def displayaddress_handler(args, client): - return displayaddress(client, desc=args.desc, path=args.path, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh, redeem_script=args.redeem_script) + return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type, redeem_script=args.redeem_script) def enumerate_handler(args): return enumerate(password=args.password) @@ -175,8 +175,7 @@ def process_commands(cli_args): group = displayaddr_parser.add_mutually_exclusive_group(required=True) group.add_argument('--desc', help='Output Descriptor. E.g. wpkh([00000000/84h/0h/0h]xpub.../0/0), where 00000000 must match --fingerprint and xpub can be obtained with getxpub. See doc/descriptors.md in Bitcoin Core') group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. m/84h/0h/0h/1/*') - displayaddr_parser.add_argument('--sh_wpkh', action='store_true', help='Display the p2sh-nested segwit address associated with this key path') - displayaddr_parser.add_argument('--wpkh', action='store_true', help='Display the bech32 version of the address associated with this key path') + displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) displayaddr_parser.add_argument('--redeem_script', help='P2SH redeem script') displayaddr_parser.set_defaults(func=displayaddress_handler) diff --git a/hwilib/commands.py b/hwilib/commands.py index b77e4876d..96395580f 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -247,17 +247,14 @@ def getdescriptors(client, account=0): return result -def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, redeem_script=None): +def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH, redeem_script=None): if path is not None: - if sh_wpkh and wpkh: - return {'error': 'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.', 'code': BAD_ARGUMENT} - return client.display_address(path, sh_wpkh, wpkh, redeem_script=redeem_script) + return client.display_address(path, addr_type, redeem_script=redeem_script) elif desc is not None: - if sh_wpkh or wpkh: - return {'error': ' `--wpkh` and `--sh_wpkh` can not be combined with --desc', 'code': BAD_ARGUMENT} if redeem_script: return {'error': ' `--redeem_script` can not be combined with --desc', 'code': BAD_ARGUMENT} descriptor = parse_descriptor(desc) + addr_type = AddressType.PKH is_sh = isinstance(descriptor, SHDescriptor) is_wsh = isinstance(descriptor, WSHDescriptor) if is_sh or is_wsh: @@ -280,7 +277,11 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede path += ',' path = path[0:-1] redeem_script += format(80 + len(descriptor.pubkeys), 'x') + 'ae' - return client.display_address(path, is_sh and is_wsh, not is_sh and is_wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None) + if is_sh and is_wsh: + addr_type = AddressType.SH_WPKH + elif not is_sh and is_wsh: + addr_type = AddressType.WPKH + return client.display_address(path, addr_type, redeem_script, descriptor=descriptor if xpubs_descriptor else None) is_wpkh = isinstance(descriptor, WPKHDescriptor) if isinstance(descriptor, PKHDescriptor) or is_wpkh: pubkey = descriptor.pubkeys[0] @@ -291,7 +292,11 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path())['xpub'] if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} - return client.display_address(pubkey.get_full_derivation_path(0), is_sh and is_wpkh, not is_sh and is_wpkh) + if is_sh and is_wpkh: + addr_type = AddressType.SH_WPKH + elif not is_sh and is_wpkh: + addr_type = AddressType.WPKH + return client.display_address(pubkey.get_full_derivation_path(0), addr_type) def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index bdd81d6fa..b6a9b9dba 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -16,6 +16,7 @@ from ..hwwclient import HardwareWalletClient, Descriptor from ..serializations import ( + AddressType, PSBT, CTxOut, is_p2pkh, @@ -329,19 +330,18 @@ def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: def display_address( self, bip32_path: str, - p2sh_p2wpkh: bool, - bech32: bool, + addr_type: AddressType, redeem_script: Optional[str] = None, descriptor: Optional[Descriptor] = None, ) -> Dict[str, str]: if redeem_script: raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") - if p2sh_p2wpkh: + if addr_type == AddressType.SH_WPKH: script_config = bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH ) - elif bech32: + elif addr_type == AddressType.WPKH: script_config = bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH ) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index a196f90d9..d5871354f 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -40,6 +40,7 @@ ExtendedKey, ) from ..serializations import ( + AddressType, PSBT, ) from hashlib import sha256 @@ -232,14 +233,14 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. @coldcard_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): + def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') - if p2sh_p2wpkh: + if addr_type == AddressType.SH_WPKH: addr_fmt = AF_P2WSH_P2SH if redeem_script else AF_P2WPKH_P2SH - elif bech32: + elif addr_type == AddressType.WPKH: addr_fmt = AF_P2WSH if redeem_script else AF_P2WPKH else: addr_fmt = AF_P2SH if redeem_script else AF_CLASSIC diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 466430d45..c8e827a0a 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -32,6 +32,7 @@ ExtendedKey, ) from ..serializations import ( + AddressType, CTransaction, hash256, is_p2pk, @@ -546,7 +547,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st return {"signature": base64.b64encode(compact_sig).decode('utf-8')} # Display address of specified type on the device. - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): + def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') # Setup a new device diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 3880ccee9..b00907422 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -29,6 +29,7 @@ ExtendedKey, ) from ..serializations import ( + AddressType, hash256, hash160, is_p2sh, @@ -346,12 +347,14 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. Only supports single-key based addresses. @ledger_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): + def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") if redeem_script is not None: raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") - output = self.app.getWalletPublicKey(keypath[2:], True, (p2sh_p2wpkh or bech32), bech32) + p2sh_p2wpkh = addr_type == AddressType.SH_WPKH + bech32 = addr_type == AddressType.WPKH + output = self.app.getWalletPublicKey(keypath[2:], True, p2sh_p2wpkh or bech32, bech32) return {'address': output['address'][12:-2]} # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. # Setup a new device diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 588e8422f..93df0b7c0 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -49,6 +49,7 @@ parse_path, ) from ..serializations import ( + AddressType, CTxOut, is_p2pkh, is_p2sh, @@ -412,7 +413,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. @trezor_exception - def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None): + def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): self._check_unlocked() # descriptor means multisig with xpubs @@ -434,9 +435,9 @@ def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, desc multisig = None # Script type - if p2sh_p2wpkh: + if addr_type == AddressType.SH_WPKH: script_type = proto.InputScriptType.SPENDP2SHWITNESS - elif bech32: + elif addr_type == AddressType.WPKH: script_type = proto.InputScriptType.SPENDWITNESS elif redeem_script: script_type = proto.InputScriptType.SPENDMULTISIG diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e27db11f9..78aa4bbeb 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -2,7 +2,7 @@ from .base58 import get_xpub_fingerprint_hex from .descriptor import Descriptor -from .serializations import PSBT +from .serializations import AddressType, PSBT class HardwareWalletClient(object): @@ -75,8 +75,7 @@ def sign_message( def display_address( self, bip32_path: str, - p2sh_p2wpkh: bool, - bech32: bool, + addr_type: AddressType, redeem_script: Optional[str] = None, descriptor: Optional[Descriptor] = None, ) -> Dict[str, str]: diff --git a/test/test_device.py b/test/test_device.py index 98ff1a3cc..d5f9f90ea 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -457,24 +457,18 @@ def test_big_tx(self): pass class TestDisplayAddress(DeviceTestCase): - def test_display_address_bad_args(self): - result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--wpkh', '--path', 'm/49h/1h/0h/0/0']) - self.assertIn('error', result) - self.assertIn('code', result) - self.assertEqual(result['code'], -7) - def test_display_address_path(self): result = self.do_command(self.dev_args + ['displayaddress', '--path', 'm/44h/1h/0h/0/0']) self.assertNotIn('error', result) self.assertNotIn('code', result) self.assertIn('address', result) - result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', 'm/49h/1h/0h/0/0']) + result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "sh_wpkh", '--path', 'm/49h/1h/0h/0/0']) self.assertNotIn('error', result) self.assertNotIn('code', result) self.assertIn('address', result) - result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', 'm/84h/1h/0h/0/0']) + result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "wpkh", '--path', 'm/84h/1h/0h/0/0']) self.assertNotIn('error', result) self.assertNotIn('code', result) self.assertIn('address', result) @@ -573,7 +567,8 @@ def test_display_address_multisig(self): else: args = ['displayaddress', '--path', path, '--redeem_script', rs] if addrtype != "pkh": - args.append("--{}".format(addrtype)) + args.append("--addr-type") + args.append(addrtype) result = self.do_command(self.dev_args + args) self.assertNotIn('error', result) From c31f4f6084c3aa51b133bf13bb4925c94fb11ad4 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Fri, 29 Jan 2021 19:55:13 +0100 Subject: [PATCH 244/634] Replace references to trezor-mcu repo with trezor-firmware repo --- test/README.md | 8 ++++---- test/setup_environment.sh | 2 +- test/test_keepkey.py | 2 +- test/test_trezor.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/README.md b/test/README.md index f28936d95..9f068c939 100644 --- a/test/README.md +++ b/test/README.md @@ -9,7 +9,7 @@ This is taken directly from the [python reference implementation](https://github - `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 uses the [Trezor One firmware emulator](https://github.com/trezor/trezor-firmware/blob/master/docs/legacy/index.md#local-development-build) and [Trezor Model T firmware emulator](https://github.com/trezor/trezor-firmware/blob/master/docs/core/emulator/index.md). It also tests usage with `bitcoind`. - `test_keepkey.py` tests the command line interface and the Keepkey implementation. It uses the [Keepkey firmware emulator](https://github.com/keepkey/keepkey-firmware/blob/master/docs/Build.md). @@ -19,7 +19,7 @@ It uses the [Coldcard simulator](https://github.com/Coldcard/firmware/tree/maste It also tests usage with `bitcoind`. `setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, and `bitcoind`. -if run in the `test/` directory, these will be built in `work/test/trezor-mcu`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively. +if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively. In order to build each simulator/emulator, you will need to use command line arguments. These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, and `--bitcoind`. If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built. @@ -57,13 +57,13 @@ pip install pipenv Clone the repository: ``` -$ git clone https://github.com/trezor/trezor-mcu/ +$ git clone https://github.com/trezor/trezor-firmware/ ``` Build the emulator in headless mode: ``` -$ cd trezor-mcu +$ cd trezor-firmware/legacy $ export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 $ script/setup $ pipenv install diff --git a/test/setup_environment.sh b/test/setup_environment.sh index cad2146ac..73d3dce69 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -51,7 +51,7 @@ mkdir -p work cd work if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then - # Clone trezor-mcu if it doesn't exist, or update it if it does + # Clone trezor-firmware if it doesn't exist, or update it if it does if [ ! -d "trezor-firmware" ]; then git clone --recursive https://github.com/trezor/trezor-firmware.git cd trezor-firmware diff --git a/test/test_keepkey.py b/test/test_keepkey.py index d6b209b7a..7683a511f 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -43,7 +43,7 @@ def start(self): # Start the Keepkey emulator self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.keepkey_log) # Wait for emulator to be up - # From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py + # From https://github.com/trezor/trezor-firmware/blob/master/legacy/script/wait_for_emulator.py sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('127.0.0.1', 21324)) sock.settimeout(0) diff --git a/test/test_trezor.py b/test/test_trezor.py index e198b1b52..2daa1369a 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -45,7 +45,7 @@ def start(self): # Start the Trezor emulator self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.emulator_log, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid) # Wait for emulator to be up - # From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py + # From https://github.com/trezor/trezor-firmware/blob/master/legacy/script/wait_for_emulator.py sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('127.0.0.1', 21324)) sock.settimeout(0) From dab29d4276704963a42baf6aacc1f255632a4fd0 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Fri, 29 Jan 2021 20:04:12 +0100 Subject: [PATCH 245/634] displayaddress: Trezor One can also show XPUBs for multisig --- docs/coldcard.md | 2 +- test/test_device.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/coldcard.md b/docs/coldcard.md index d90fa3541..b5ca98c9c 100644 --- a/docs/coldcard.md +++ b/docs/coldcard.md @@ -25,5 +25,5 @@ The `backup` command will create a backup file in the current working directory. ## Caveat for `signtx` - The Coldcard firmware only supports signing single key and multisig transactions. It cannot sign arbitrary scripts. -- Multsigs need to be registered on the device before a transaction spending that multisig will be signed by the device. +- Multisigs need to be registered on the device before a transaction spending that multisig will be signed by the device. - Multisigs must use BIP 67. This can be accomplished in Bitcoin Core using the `sortedmulti()` descriptor, available in Bitcoin Core 0.20. diff --git a/test/test_device.py b/test/test_device.py index 657e3c733..8b6241a13 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -18,7 +18,7 @@ from hwilib.serializations import PSBT SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} -SUPPORTS_XPUB_MS_DISPLAY = {'trezor_t'} +SUPPORTS_XPUB_MS_DISPLAY = {'trezor_1', 'trezor_t'} # Class for emulator control class DeviceEmulator(): @@ -589,7 +589,7 @@ def test_display_address_multisig(self): def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: - raise unittest.SkipTest("{} does not support multsig display with xpubs".format(self.full_type)) + raise unittest.SkipTest("{} does not support multisig display with xpubs".format(self.full_type)) account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/48h/1h/0h/0h'])['xpub'] desc = 'wsh(multi(2,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/0/0,[' + self.fingerprint + '/48h/1h/0h/0h]' + account_xpub + '/1/0))' From 267f2af7304354a90899c3cc517abf7fedc8891e Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Fri, 29 Jan 2021 20:54:02 +0100 Subject: [PATCH 246/634] tests: refactor Trezor model handling --- test/run_tests.py | 4 ++-- test/test_trezor.py | 44 ++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/test/run_tests.py b/test/run_tests.py index fa4eafad4..39fa5cae7 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -98,9 +98,9 @@ if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, rpc, userpass, args.interface) if success and args.trezor_1: - success &= trezor_test_suite(args.trezor_1_path, rpc, userpass, args.interface) + success &= trezor_test_suite(args.trezor_1_path, rpc, userpass, args.interface, '1') if success and args.trezor_t: - success &= trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, True) + success &= trezor_test_suite(args.trezor_t_path, rpc, userpass, args.interface, 't') if success and args.keepkey: success &= keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface) if success and args.ledger: diff --git a/test/test_trezor.py b/test/test_trezor.py index 2daa1369a..dfe7e3a3e 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -23,6 +23,8 @@ from types import MethodType +TREZOR_MODELS = {'1', 't'} + def get_pin(self, code=None): if self.pin: return self.debuglink.encode_pin(self.pin) @@ -30,18 +32,19 @@ def get_pin(self, code=None): return self.debuglink.read_pin_encoded() class TrezorEmulator(DeviceEmulator): - def __init__(self, path, model_t): + def __init__(self, path, model): + assert model in TREZOR_MODELS self.emulator_path = path self.emulator_proc = None - self.model_t = model_t + self.model = model self.emulator_log = None try: - os.unlink('trezor-{}-emulator.stdout'.format('t' if model_t else '1')) + os.unlink('trezor-{}-emulator.stdout'.format(self.model)) except FileNotFoundError: pass def start(self): - self.emulator_log = open('trezor-{}-emulator.stdout'.format('t' if self.model_t else '1'), 'a') + self.emulator_log = open('trezor-{}-emulator.stdout'.format(self.model), 'a') # Start the Trezor emulator self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=self.emulator_log, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid) # Wait for emulator to be up @@ -77,9 +80,9 @@ def stop(self): os.waitpid(self.emulator_proc.pid, 0) # Clean up emulator image - if self.model_t: + if self.model == 't': emulator_img = "/var/tmp/trezor.flash" - else: + else: # self.model == '1' emulator_img = os.path.dirname(self.emulator_path) + "/emulator.img" if os.path.isfile(emulator_img): @@ -127,10 +130,10 @@ def do_command(self, args): return process_commands(args) def __str__(self): - return 'trezor_{}: {}'.format('t' if self.emulator.model_t else '1', super().__str__()) + return 'trezor_{}: {}'.format(self.emulator.model, super().__str__()) def __repr__(self): - return 'trezor_{}: {}'.format('t' if self.emulator.model_t else '1', super().__repr__()) + return 'trezor_{}: {}'.format(self.emulator.model, super().__repr__()) def setUp(self): self.client = self.emulator.start() @@ -280,7 +283,7 @@ def test_passphrase(self): self.assertFalse(dev['needs_passphrase_sent']) fpr = dev['fingerprint'] - if self.emulator.model_t: + if self.emulator.model == 't': # Trezor T: A different passphrase would not change the fingerprint result = self.do_command(self.dev_args + ['-p', 'pass2', 'enumerate']) for dev in result: @@ -319,7 +322,8 @@ def test_passphrase(self): self.assertFalse(dev['needs_passphrase_sent']) self.assertEquals(dev['fingerprint'], '95d8f670') -def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): +def trezor_test_suite(emulator, rpc, userpass, interface, model): + assert model in TREZOR_MODELS # Redirect stderr to /dev/null as it's super spammy sys.stderr = open(os.devnull, 'w') @@ -328,12 +332,8 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): path = 'udp:127.0.0.1:21324' fingerprint = '95d8f670' master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH' - dev_emulator = TrezorEmulator(emulator, model_t) - - if model_t: - full_type = 'trezor_t' - else: - full_type = 'trezor_1' + dev_emulator = TrezorEmulator(emulator, model) + full_type = 'trezor_{}'.format(model) # Generic Device tests suite = unittest.TestSuite() @@ -344,11 +344,9 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface)) - if not model_t: - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_1_simulator', full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_{}_simulator'.format(model), full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) + if model != 't': suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface)) - else: - suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'trezor_t_simulator', full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) sys.stderr = sys.__stderr__ @@ -359,10 +357,12 @@ def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False): parser.add_argument('emulator', help='Path to the Trezor emulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') - parser.add_argument('--model_t', help='The emulator is for the Trezor T', action='store_true') + group = parser.add_argument_group() + group.add_argument('--model_1', help='The emulator is for the Trezor One', action='store_const', const='1', dest='model') + group.add_argument('--model_t', help='The emulator is for the Trezor T', action='store_const', const='t', dest='model') args = parser.parse_args() # Start bitcoind rpc, userpass = start_bitcoind(args.bitcoind) - sys.exit(not trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model_t)) + sys.exit(not trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model)) From b2eabc6f9d75b3b31b10489e3abfb65a0c0478d8 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 15:26:37 -0500 Subject: [PATCH 247/634] ledger: Fix KeyOriginInfo is not subscritable error Also adds a test for this case. --- hwilib/devices/ledger.py | 6 +++--- test/data/speculos-automation.json | 2 +- test/test_device.py | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 3880ccee9..553d8d944 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -206,12 +206,12 @@ def sign_tx(self, tx): # Find which wallet key could be change based on hdsplit: m/.../1/k # Wallets shouldn't be sending to change address as user action # otherwise this will get confused - for pubkey, path in tx.outputs[i_num].hd_keypaths.items(): - if path.fingerprint == master_fpr and len(path.path) > 1 and path[-1] == 1: + for pubkey, origin in tx.outputs[i_num].hd_keypaths.items(): + if origin.fingerprint == master_fpr and len(origin.path) > 1 and origin.path[-2] == 1: # For possible matches, check if pubkey matches possible template if hash160(pubkey) in txout.scriptPubKey or hash160(bytearray.fromhex("0014") + hash160(pubkey)) in txout.scriptPubKey: change_path = '' - for index in path[1:]: + for index in origin.path[1:]: change_path += str(index) + "/" change_path = change_path[:-1] diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 1ada0d43e..dd17ecaf3 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -2,7 +2,7 @@ "version": 1, "rules": [ { - "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if you're).*", + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if you're|The change path|Change path).*", "actions": [ [ "button", 2, true ], [ "button", 2, false ] diff --git a/test/test_device.py b/test/test_device.py index 657e3c733..12574d5a5 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -374,7 +374,7 @@ def _test_signtx(self, input_type, multisig, external): self.assertTrue(multi_result[2]['success']) in_amt = 3 - out_amt = in_amt // 3 + out_amt = in_amt // 3 * 0.9 number_inputs = 0 # Single-sig if input_type == 'segwit' or input_type == 'all': @@ -399,9 +399,12 @@ def _test_signtx(self, input_type, multisig, external): # Spend different amounts, requiring 1 to 3 inputs for i in range(number_inputs): # Create a psbt spending the above + change_addr = self.wrpc.getrawchangeaddress() if i == number_inputs - 1: - self.assertTrue((i + 1) * in_amt == self.wrpc.getbalance("*", 0, True)) - psbt = self.wrpc.walletcreatefundedpsbt([], [{self.wpk_rpc.getnewaddress('', 'legacy'): (i + 1) * out_amt}, {self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): (i + 1) * out_amt}, {self.wpk_rpc.getnewaddress('', 'bech32'): (i + 1) * out_amt}], 0, {'includeWatching': True, 'subtractFeeFromOutputs': [0, 1, 2]}, True) + self.assertEqual((i + 1) * in_amt, self.wrpc.getbalance("*", 0, True)) + change_addr = self.wpk_rpc.getrawchangeaddress() + out_val = (i + 1) * out_amt + psbt = self.wrpc.walletcreatefundedpsbt([], [{self.wpk_rpc.getnewaddress('', 'legacy'): out_val}, {self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): out_val}, {self.wpk_rpc.getnewaddress('', 'bech32'): out_val}], 0, {'includeWatching': True, "changePosition": 3, "changeAddress": change_addr}, True) if external: # Sign with unknown inputs in two steps From 9cba0b889cf2a1bf3186c86cacc110671bf76f2e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sat, 30 Jan 2021 18:26:07 -0500 Subject: [PATCH 248/634] ci: print out the right files on failure The on_failure script was looking for the .stdout and .stderr files in the wrong directory. When a job fails, it should now print out the right files for debugging. --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index b06e07303..9be23ba04 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -186,7 +186,7 @@ dist_test_task: install_script: poetry install test_script: cd test; poetry run ./run_tests.py $DEVICE --interface=bindist --device-only; cd .. on_failure: - failed_script: tail -v -n +1 *.std* + failed_script: tail -v -n +1 test/*.std* device_test_task: matrix: @@ -203,4 +203,4 @@ device_test_task: install_script: poetry install test_script: cd test; poetry run ./run_tests.py $DEVICE --interface=$INTERFACE --device-only; cd .. on_failure: - failed_script: tail -v -n +1 *.std* + failed_script: tail -v -n +1 test/*.std* From 0ba31614cfd584b6dddb6e5bff965cf544ef3099 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Sun, 31 Jan 2021 15:10:44 +0100 Subject: [PATCH 249/634] trezor: Enable processing of OP_RETURN outputs --- hwilib/devices/trezor.py | 3 +++ hwilib/serializations.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 93df0b7c0..25898b344 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -328,6 +328,9 @@ def ignore_input(): txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version) elif out.is_p2sh(): txoutput.address = to_address(out.scriptPubKey[2:22], p2sh_version) + elif out.is_opreturn(): + txoutput.script_type = proto.OutputScriptType.PAYTOOPRETURN + txoutput.op_return_data = out.scriptPubKey[2:] else: wit, ver, prog = out.is_witness() if wit: diff --git a/hwilib/serializations.py b/hwilib/serializations.py index e2a5f72ad..990f3669f 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -283,6 +283,9 @@ def __repr__(self) -> str: self.nSequence) +def is_opreturn(script: bytes) -> bool: + return script[0] == 0x6a + def is_p2sh(script: bytes) -> bool: return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 @@ -336,6 +339,9 @@ def serialize(self) -> bytes: r += ser_string(self.scriptPubKey) return r + def is_opreturn(self) -> bool: + return is_opreturn(self.scriptPubKey) + def is_p2sh(self) -> bool: return is_p2sh(self.scriptPubKey) From 22598ce50e6cb16e32f003572965eb51b077519e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sun, 31 Jan 2021 14:56:27 -0500 Subject: [PATCH 250/634] test OP_RETURN outputs in psbts --- test/test_device.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 5c5aff14d..0a8323595 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -354,7 +354,7 @@ def get_pubkeys(t): return sh_desc, sh_ms_info["address"], sh_wsh_desc, sh_wsh_ms_info["address"], wsh_desc, wsh_ms_info["address"] - def _test_signtx(self, input_type, multisig, external): + def _test_signtx(self, input_type, multisig, external, op_return: bool): # Import some keys to the watch only wallet and send coins to them keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '30', '50']) import_result = self.wrpc.importdescriptors(keypool_desc) @@ -404,7 +404,14 @@ def _test_signtx(self, input_type, multisig, external): self.assertEqual((i + 1) * in_amt, self.wrpc.getbalance("*", 0, True)) change_addr = self.wpk_rpc.getrawchangeaddress() out_val = (i + 1) * out_amt - psbt = self.wrpc.walletcreatefundedpsbt([], [{self.wpk_rpc.getnewaddress('', 'legacy'): out_val}, {self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): out_val}, {self.wpk_rpc.getnewaddress('', 'bech32'): out_val}], 0, {'includeWatching': True, "changePosition": 3, "changeAddress": change_addr}, True) + outputs = [ + {self.wpk_rpc.getnewaddress('', 'legacy'): out_val}, + {self.wpk_rpc.getnewaddress('', 'p2sh-segwit'): out_val}, + {self.wpk_rpc.getnewaddress('', 'bech32'): out_val} + ] + if op_return: + outputs.append({"data": "000102030405060708090a0b0c0d0e0f10111213141516171819101a1b1c1d1e1f"}) + psbt = self.wrpc.walletcreatefundedpsbt([], outputs, 0, {'includeWatching': True, "changePosition": 3, "changeAddress": change_addr}, True) if external: # Sign with unknown inputs in two steps @@ -420,15 +427,17 @@ def test_signtx(self): supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'} supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} + supports_op_return = {'ledger', 'digitalbitbox', 'trezor_1', 'trezor_t', 'keepkey'} multisig = self.full_type in supports_multisig external = self.full_type in supports_external + op_return = self.full_type in supports_op_return with self.subTest(addrtype="legacy", multisig=multisig, external=external): - self._test_signtx("legacy", multisig, external) + self._test_signtx("legacy", multisig, external, op_return) with self.subTest(addrtype="segwit", multisig=multisig, external=external): - self._test_signtx("segwit", multisig, external) + self._test_signtx("segwit", multisig, external, op_return) if self.full_type in supports_mixed: with self.subTest(addrtype="all", multisig=multisig, external=external): - self._test_signtx("all", multisig, external) + self._test_signtx("all", multisig, external, op_return) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): From 6403389084e26519e3ac0edea7302654283678db Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Sun, 31 Jan 2021 23:10:29 +0100 Subject: [PATCH 251/634] Move SUPPORTS_* closer together --- test/test_device.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 0a8323595..127685201 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -19,6 +19,10 @@ SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} SUPPORTS_XPUB_MS_DISPLAY = {'trezor_1', 'trezor_t'} +SUPPORTS_MIXED = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'} +SUPPORTS_MULTISIG = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} +SUPPORTS_EXTERNAL = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} +SUPPORTS_OP_RETURN = {'ledger', 'digitalbitbox', 'trezor_1', 'trezor_t', 'keepkey'} # Class for emulator control class DeviceEmulator(): @@ -424,18 +428,14 @@ def _test_signtx(self, input_type, multisig, external, op_return: bool): # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): - supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'} - supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} - supports_external = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'} - supports_op_return = {'ledger', 'digitalbitbox', 'trezor_1', 'trezor_t', 'keepkey'} - multisig = self.full_type in supports_multisig - external = self.full_type in supports_external - op_return = self.full_type in supports_op_return + multisig = self.full_type in SUPPORTS_MULTISIG + external = self.full_type in SUPPORTS_EXTERNAL + op_return = self.full_type in SUPPORTS_OP_RETURN with self.subTest(addrtype="legacy", multisig=multisig, external=external): self._test_signtx("legacy", multisig, external, op_return) with self.subTest(addrtype="segwit", multisig=multisig, external=external): self._test_signtx("segwit", multisig, external, op_return) - if self.full_type in supports_mixed: + if self.full_type in SUPPORTS_MIXED: with self.subTest(addrtype="all", multisig=multisig, external=external): self._test_signtx("all", multisig, external, op_return) From 7844bfada6f0e464764db650f3661ce93c6c192e Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 1 Feb 2021 18:11:02 +0100 Subject: [PATCH 252/634] bitbox02: update to v5.2.0 Updated pyproject.toml and setup.py, then: `poetry update bitbox02 --lock`. - Support for antiklepto - Small function signature change in restore_from_mnemonic - Add version upper bound following semver --- hwilib/devices/bitbox02.py | 3 ++- poetry.lock | 3 +-- pyproject.toml | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index b6a9b9dba..e5559fa5d 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -627,4 +627,5 @@ def restore_device( if label: bb02.set_device_name(label) - return {"success": bb02.restore_from_mnemonic()} + bb02.restore_from_mnemonic() + return {"success": True} diff --git a/poetry.lock b/poetry.lock index fe1e1d896..f446c45b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,7 +374,7 @@ qt = ["pyside2"] [metadata] lock-version = "1.1" python-versions = "^3.6,<3.10" -content-hash = "fae8cd4ec5fae48e4eed77a42ff209233bf32fff1e7a79854a492f7c06582c4f" +content-hash = "569af9248ab36c24591d50a64650e507928acfcfea8258e08dde06a9659564ab" [metadata.files] altgraph = [ @@ -527,7 +527,6 @@ mypy-extensions = [ ] noiseprotocol = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, - {file = "noiseprotocol-0.3.1.tar.gz", hash = "sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645"}, ] pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, diff --git a/pyproject.toml b/pyproject.toml index 22d863d34..ef806e288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ mnemonic = "~0" typing-extensions = "^3.7" libusb1 = "^1.7" pyside2 = { version = "^5.14.0", optional = true } -bitbox02 = ">=5.1.0" +bitbox02 = ">=5.2.0,<6.0.0" [tool.poetry.extras] qt = ["pyside2"] diff --git a/setup.py b/setup.py index 7e7d27f0c..2c046d7d1 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ modules = \ ['hwi', 'hwi-qt'] install_requires = \ -['bitbox02>=5.1.0', +['bitbox02>=5.2.0,<6.0.0', 'ecdsa>=0,<1', 'hidapi>=0,<1', 'libusb1>=1.7,<2.0', From 35ec4dec1d49dfe94d1b7c5aa4a1bc892a1cdeee Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Mon, 1 Feb 2021 14:06:16 +0800 Subject: [PATCH 253/634] Add some dockerfile steps for local testing This allows testers to quickly build images they can test local changes against. Follow-up step would be to use this as a base image for all cirrus jobs. --- ci/cirrus.Dockerfile | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile index f01ced7c6..f4c5dedc3 100644 --- a/ci/cirrus.Dockerfile +++ b/ci/cirrus.Dockerfile @@ -33,8 +33,48 @@ RUN apt-get install -y \ libboost-thread-dev \ protobuf-compiler \ cython3 + RUN pip install poetry flake8 +#################### +# Local build/test steps +# ----------------- +# To install all simulators/tests locally, uncomment the block below, +# then build the docker image and interactively run the tests +# as needed. +# e.g., +# docker build -f ci/cirrus.Dockerfile -t hwi_test . +# docker run -it --entrypoint /bin/bash hwi_tst +# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only +#################### + +#################### +#ENV EMAIL=email +#COPY pyproject.toml pyproject.toml +#RUN poetry run pip install construct pyelftools mnemonic jsonschema +# +## Set up environments first to take advantage of layer caching +#RUN mkdir test +#COPY test/setup_environment.sh test/setup_environment.sh +#COPY test/data/coldcard-multisig.patch test/data/coldcard-multisig.patch +## One by one to allow for intermediate caching of successful builds +#RUN cd test; ./setup_environment.sh --trezor-1 +#RUN cd test; ./setup_environment.sh --trezor-t +#RUN cd test; ./setup_environment.sh --coldcard +#RUN cd test; ./setup_environment.sh --bitbox01 +#RUN cd test; ./setup_environment.sh --ledger +#RUN cd test; ./setup_environment.sh --keepkey +#RUN cd test; ./setup_environment.sh --bitcoind +# +## Once everything has been built, put rest of files in place +## which have higher turn-over. +#COPY test/ test/ +#COPY hwi.py hwi-qt.py README.md / +#COPY hwilib/ /hwilib/ +#RUN poetry install +# +#################### + ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 ENV LANGUAGE=C.UTF-8 From e459d6b24d8b364eaca39e3c6cb1f98eea5d6317 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 13:12:12 -0500 Subject: [PATCH 254/634] Parse xpubs in descriptors --- hwilib/base58.py | 7 ++++++- hwilib/descriptor.py | 11 ++++++++++- hwilib/key.py | 3 +++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/hwilib/base58.py b/hwilib/base58.py index efc8d0346..b9f604562 100644 --- a/hwilib/base58.py +++ b/hwilib/base58.py @@ -9,8 +9,13 @@ # import hashlib + from binascii import hexlify, unhexlify from typing import List + +from .errors import BadArgumentError + + b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' def sha256(s: bytes) -> bytes: @@ -52,7 +57,7 @@ def decode(s: str) -> bytes: for c in s: n *= 58 if c not in b58_digits: - raise ValueError('Character %r is not a valid base58 character' % c) + raise BadArgumentError('Character %r is not a valid base58 character' % c) digit = b58_digits.index(c) n += digit diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 261429bc7..3e7ab834f 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,4 +1,4 @@ -from .key import KeyOriginInfo +from .key import ExtendedKey, KeyOriginInfo, parse_path from enum import Enum from typing import ( @@ -68,6 +68,15 @@ def __init__( self.pubkey = pubkey self.deriv_path = deriv_path + # Make ExtendedKey from pubkey if it isn't hex + self.extkey = None + try: + unhexlify(self.pubkey) + # Is hex, normal pubkey + except Exception: + # Not hex, maybe xpub + self.extkey = ExtendedKey.deserialize(self.pubkey) + @classmethod def parse(cls, s: str) -> 'PubkeyProvider': origin = None diff --git a/hwilib/key.py b/hwilib/key.py index 62be8cb15..385b51686 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from . import base58 +from .errors import BadArgumentError import binascii import hmac @@ -109,6 +110,8 @@ def deserialize(cls, xpub: str) -> 'ExtendedKey': data = base58.decode(xpub)[:-4] # Decoded xpub without checksum version = data[0:4] + if version not in [ExtendedKey.MAINNET_PRIVATE, ExtendedKey.MAINNET_PUBLIC, ExtendedKey.TESTNET_PRIVATE, ExtendedKey.TESTNET_PUBLIC]: + raise BadArgumentError(f"Extended key magic of {version.hex()} is invalid") is_private = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE depth = data[4] parent_fingerprint = data[5:9] From 03f5033548b2599cbad2df14f56f0e447583211d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 20 Jan 2021 13:12:42 -0500 Subject: [PATCH 255/634] Add Descriptor.expand to get the scripts for a descriptor --- hwilib/descriptor.py | 58 ++++++++++++++++++++++++++++ test/test_descriptor.py | 84 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 3e7ab834f..39d44e37f 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,5 +1,8 @@ from .key import ExtendedKey, KeyOriginInfo, parse_path +from .serializations import hash160, sha256 +from binascii import unhexlify +from collections import namedtuple from enum import Enum from typing import ( List, @@ -9,6 +12,8 @@ # From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp +ExpandedScripts = namedtuple("ExpandedScripts", ["output_script", "redeem_script", "witness_script"]) + def PolyMod(c: int, val: int) -> int: c0 = c >> 35 c = ((c & 0x7ffffffff) << 5) ^ val @@ -104,6 +109,19 @@ def to_string(self) -> str: s += self.deriv_path return s + def get_pubkey_bytes(self, pos: int) -> bytes: + if self.extkey is not None: + if self.deriv_path is not None: + path_str = self.deriv_path[1:] + if path_str[-1] == "*": + path_str = path_str[-1] + str(pos) + path = parse_path(path_str) + child_key = self.extkey.derive_pub_path(path) + return child_key.pubkey + else: + return self.extkey.pubkey + return unhexlify(self.pubkey) + def get_full_derivation_path(self, pos: int) -> str: """ Returns the full derivation path at the given position, including the origin @@ -139,6 +157,12 @@ def to_string_no_checksum(self) -> str: def to_string(self) -> str: return AddChecksum(self.to_string_no_checksum()) + def expand(self, pos: int) -> "ExpandedScripts": + """ + Returns the scripts for a descriptor at the given `pos` for ranged descriptors. + """ + raise NotImplementedError("The Descriptor base class does not implement this method") + class PKHDescriptor(Descriptor): def __init__( @@ -147,6 +171,10 @@ def __init__( ) -> None: super().__init__([pubkey], None, "pkh") + def expand(self, pos: int) -> "ExpandedScripts": + script = b"\x76\xa9\x14" + hash160(self.pubkeys[0].get_pubkey_bytes(pos)) + b"\x88\xac" + return ExpandedScripts(script, None, None) + class WPKHDescriptor(Descriptor): def __init__( @@ -155,6 +183,10 @@ def __init__( ) -> None: super().__init__([pubkey], None, "wpkh") + def expand(self, pos: int) -> "ExpandedScripts": + script = b"\x00\x14" + hash160(self.pubkeys[0].get_pubkey_bytes(pos)) + return ExpandedScripts(script, None, None) + class MultisigDescriptor(Descriptor): def __init__( @@ -171,6 +203,20 @@ def __init__( def to_string_no_checksum(self) -> str: return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) + def expand(self, pos: int) -> "ExpandedScripts": + if self.thresh > 16: + m = b"\x01" + self.thresh.to_bytes(1, "big") + else: + m = (self.thresh + 0x50).to_bytes(1, "big") if self.thresh > 0 else b"\x00" + n = (len(self.pubkeys) + 0x50).to_bytes(1, "big") if len(self.pubkeys) > 0 else b"\x00" + script: bytes = m + for p in self.pubkeys: + pk = p.get_pubkey_bytes(pos) + script += len(pk).to_bytes(1, "big") + pk + script += n + b"\xae" + + return ExpandedScripts(script, None, None) + class SHDescriptor(Descriptor): def __init__( @@ -179,6 +225,12 @@ def __init__( ) -> None: super().__init__([], subdescriptor, "sh") + def expand(self, pos: int) -> "ExpandedScripts": + assert self.subdescriptor + redeem_script, _, witness_script = self.subdescriptor.expand(pos) + script = b"\xa9\x14" + hash160(redeem_script) + b"\x87" + return ExpandedScripts(script, redeem_script, witness_script) + class WSHDescriptor(Descriptor): def __init__( @@ -187,6 +239,12 @@ def __init__( ) -> None: super().__init__([], subdescriptor, "wsh") + def expand(self, pos: int) -> "ExpandedScripts": + assert self.subdescriptor + witness_script, _, _ = self.subdescriptor.expand(pos) + script = b"\x00\x20" + sha256(witness_script) + return ExpandedScripts(script, None, witness_script) + def _get_func_expr(s: str) -> Tuple[str, str]: """ diff --git a/test/test_descriptor.py b/test/test_descriptor.py index 0760c9c2b..e65495cc7 100755 --- a/test/test_descriptor.py +++ b/test/test_descriptor.py @@ -3,9 +3,14 @@ from hwilib.descriptor import ( parse_descriptor, MultisigDescriptor, + SHDescriptor, + PKHDescriptor, WPKHDescriptor, WSHDescriptor, ) + +from binascii import unhexlify + import unittest class TestDescriptor(unittest.TestCase): @@ -18,6 +23,10 @@ def test_parse_descriptor_with_origin(self): self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) def test_parse_multisig_descriptor_with_origin(self): d = "wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))" @@ -33,8 +42,50 @@ def test_parse_multisig_descriptor_with_origin(self): self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") self.assertEqual(desc.subdescriptor.pubkeys[1].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + + d = "sh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, SHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptor, MultisigDescriptor)) + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptor.pubkeys[0].deriv_path, "/0/0") + + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptor.pubkeys[1].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("a91495ee6326805b1586bb821fc3c0eeab2c68441b4187")) + self.assertEqual(e.redeem_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + self.assertEqual(e.witness_script, None) + + d = "sh(wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, SHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptor, WSHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptor.subdescriptor, MultisigDescriptor)) + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].deriv_path, "/0/0") self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("a914779ae0f6958e98b997cc177f9b554289905fbb5587")) + self.assertEqual(e.redeem_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59")) + self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) def test_parse_descriptor_without_origin(self): d = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" @@ -44,6 +95,10 @@ def test_parse_descriptor_without_origin(self): self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) def test_parse_descriptor_with_origin_fingerprint_only(self): d = "wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" @@ -54,23 +109,44 @@ def test_parse_descriptor_with_origin_fingerprint_only(self): self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) def test_parse_descriptor_with_key_at_end_with_origin(self): - d = "wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + d = "wpkh([00000001/84'/1'/0'/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WPKHDescriptor)) self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'/0/0") - self.assertEqual(desc.pubkeys[0].pubkey, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + d = "pkh([00000001/84'/1'/0'/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, PKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'/0/0") + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(0) + self.assertEqual(e.output_script, unhexlify("76a914d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa88ac")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) def test_parse_descriptor_with_key_at_end_without_origin(self): - d = "wpkh(0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + d = "wpkh(02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WPKHDescriptor)) self.assertEqual(desc.pubkeys[0].origin, None) - self.assertEqual(desc.pubkeys[0].pubkey, "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) self.assertEqual(desc.to_string_no_checksum(), d) From 7650a57a1ab057cfcd1b2cc2b878376ce8844929 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 27 Jan 2021 17:22:32 -0500 Subject: [PATCH 256/634] Refactor display_address into singlesig and multisig variants --- hwilib/cli.py | 3 +- hwilib/commands.py | 24 ++------ hwilib/descriptor.py | 21 +++++++ hwilib/devices/bitbox02.py | 19 +++--- hwilib/devices/coldcard.py | 101 ++++++++++++++++---------------- hwilib/devices/digitalbitbox.py | 12 +++- hwilib/devices/ledger.py | 24 ++++++-- hwilib/devices/trezor.py | 92 +++++++++++++++++++---------- hwilib/hwwclient.py | 31 +++++++--- hwilib/key.py | 10 ++++ test/test_device.py | 39 +++++------- 11 files changed, 226 insertions(+), 150 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 91b6c567d..61d03d7fb 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -41,7 +41,7 @@ def backup_device_handler(args, client): return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) def displayaddress_handler(args, client): - return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type, redeem_script=args.redeem_script) + return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type) def enumerate_handler(args): return enumerate(password=args.password) @@ -176,7 +176,6 @@ def process_commands(cli_args): group.add_argument('--desc', help='Output Descriptor. E.g. wpkh([00000000/84h/0h/0h]xpub.../0/0), where 00000000 must match --fingerprint and xpub can be obtained with getxpub. See doc/descriptors.md in Bitcoin Core') group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. m/84h/0h/0h/1/*') displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) - displayaddr_parser.add_argument('--redeem_script', help='P2SH redeem script') displayaddr_parser.set_defaults(func=displayaddress_handler) setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p. Requires interactive mode') diff --git a/hwilib/commands.py b/hwilib/commands.py index 96395580f..f2b1fbce3 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -247,12 +247,10 @@ def getdescriptors(client, account=0): return result -def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH, redeem_script=None): +def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH): if path is not None: - return client.display_address(path, addr_type, redeem_script=redeem_script) + return client.display_singlesig_address(path, addr_type) elif desc is not None: - if redeem_script: - return {'error': ' `--redeem_script` can not be combined with --desc', 'code': BAD_ARGUMENT} descriptor = parse_descriptor(desc) addr_type = AddressType.PKH is_sh = isinstance(descriptor, SHDescriptor) @@ -263,25 +261,11 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres is_wsh = True descriptor = descriptor.subdescriptor if isinstance(descriptor, MultisigDescriptor): - path = '' - redeem_script = format(80 + int(descriptor.thresh), 'x') - xpubs_descriptor = False - for p in descriptor.pubkeys: - path += p.origin.to_string() - if not p.deriv_path: - redeem_script += format(len(p.pubkey) // 2, 'x') - redeem_script += p.pubkey - else: - path += p.deriv_path - xpubs_descriptor = True - path += ',' - path = path[0:-1] - redeem_script += format(80 + len(descriptor.pubkeys), 'x') + 'ae' if is_sh and is_wsh: addr_type = AddressType.SH_WPKH elif not is_sh and is_wsh: addr_type = AddressType.WPKH - return client.display_address(path, addr_type, redeem_script, descriptor=descriptor if xpubs_descriptor else None) + return client.display_multisig_address(descriptor.thresh, descriptor.pubkeys, addr_type) is_wpkh = isinstance(descriptor, WPKHDescriptor) if isinstance(descriptor, PKHDescriptor) or is_wpkh: pubkey = descriptor.pubkeys[0] @@ -296,7 +280,7 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres addr_type = AddressType.SH_WPKH elif not is_sh and is_wpkh: addr_type = AddressType.WPKH - return client.display_address(pubkey.get_full_derivation_path(0), addr_type) + return client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type) def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 39d44e37f..7352e2aef 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -132,6 +132,27 @@ def get_full_derivation_path(self, pos: int) -> str: path = path[:-1] + str(pos) return path + def get_full_derivation_int_list(self, pos: int) -> List[int]: + """ + Returns the full derivation path as an integer list at the given position. + Includes the origin and master key fingerprint as an int + """ + path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] + if self.deriv_path is not None: + der_split = self.deriv_path.split("/") + for p in der_split: + if not p: + continue + if p == "*": + i = pos + elif p[-1] in "'phHP": + assert len(p) >= 2 + i = int(p[:-1]) | 0x80000000 + else: + i = int(p) + path.append(i) + return path + def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index b6a9b9dba..4dd93b5ae 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -14,7 +14,8 @@ import sys from functools import wraps -from ..hwwclient import HardwareWalletClient, Descriptor +from ..descriptor import PubkeyProvider +from ..hwwclient import HardwareWalletClient from ..serializations import ( AddressType, PSBT, @@ -327,16 +328,11 @@ def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: return {"xpub": xpub} @bitbox02_exception - def display_address( + def display_singlesig_address( self, bip32_path: str, addr_type: AddressType, - redeem_script: Optional[str] = None, - descriptor: Optional[Descriptor] = None, ) -> Dict[str, str]: - if redeem_script: - raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") - if addr_type == AddressType.SH_WPKH: script_config = bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH @@ -357,6 +353,15 @@ def display_address( ) return {"address": address} + @bitbox02_exception + def display_multisig_address( + self, + threshold: int, + pubkeys: List[PubkeyProvider], + addr_type: AddressType, + ) -> Dict[str, str]: + raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") + @bitbox02_exception def sign_tx(self, psbt: PSBT) -> Dict[str, str]: def find_our_key( diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index d5871354f..f5270b8e2 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -1,7 +1,12 @@ # Coldcard interaction script -from typing import Dict, Union +from typing import ( + Dict, + List, + Union, +) +from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -51,35 +56,12 @@ import sys import time import struct -from binascii import hexlify, a2b_hex, b2a_hex +from binascii import hexlify, b2a_hex CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock' # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md -def str_to_int_path(xfp, path): - # convert text m/34'/33/44 into BIP174 binary compat format - # - include hex for fingerprint (m) as first arg - - rv = [struct.unpack('= 2, i - here = int(i[:-1]) | 0x80000000 - else: - here = int(i) - assert 0 <= here < 0x80000000, here - - rv.append(here) - - return rv - - def coldcard_exception(f): def func(*args, **kwargs): try: @@ -233,49 +215,64 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. @coldcard_exception - def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): + def display_singlesig_address( + self, + keypath: str, + addr_type: AddressType, + ) -> Dict[str, str]: self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') if addr_type == AddressType.SH_WPKH: - addr_fmt = AF_P2WSH_P2SH if redeem_script else AF_P2WPKH_P2SH + addr_fmt = AF_P2WPKH_P2SH elif addr_type == AddressType.WPKH: - addr_fmt = AF_P2WSH if redeem_script else AF_P2WPKH + addr_fmt = AF_P2WPKH else: - addr_fmt = AF_P2SH if redeem_script else AF_CLASSIC + addr_fmt = AF_CLASSIC + + payload = CCProtocolPacker.show_address(keypath, addr_fmt=addr_fmt) + + address = self.device.send_recv(payload, timeout=None) - if redeem_script: - keypaths = keypath.split(',') - script = a2b_hex(redeem_script) + if self.device.is_simulator: + self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) + return {'address': address} - N = len(keypaths) + @coldcard_exception + def display_multisig_address( + self, + threshold: int, + pubkeys: List[PubkeyProvider], + addr_type: AddressType, + ) -> Dict[str, str]: + self.device.check_mitm() - if not 1 <= N <= 15: - raise BadArgumentError("Must provide 1 to 15 keypaths to display a multisig address") + if addr_type == AddressType.SH_WPKH: + addr_fmt = AF_P2WSH_P2SH + elif addr_type == AddressType.WPKH: + addr_fmt = AF_P2WSH + else: + addr_fmt = AF_P2SH - min_signers = script[0] - 80 - if not 1 <= min_signers <= N: - raise BadArgumentError("Either the redeem script provided is invalid or the keypaths provided are insufficient") + if not 1 <= len(pubkeys) <= 15: + raise BadArgumentError("Must provide 1 to 15 keypaths to display a multisig address") - if not script[-1] == 0xAE: - raise BadArgumentError("The redeem script provided is not a multisig. Only multisig scripts can be displayed.") + redeem_script = (80 + int(threshold)).to_bytes(1, byteorder="little") - if not script[-2] == 80 + N: - raise BadArgumentError("Invalid redeem script, second last byte should encode N") + if not 1 <= threshold <= len(pubkeys): + raise BadArgumentError("Either the redeem script provided is invalid or the keypaths provided are insufficient") - xfp_paths = [] - for xfp in keypaths: - if '/' not in xfp: - raise BadArgumentError('Invalid keypath. Needs a XFP/path: ' + xfp) - xfp, p = xfp.split('/', 1) + xfp_paths = [] + for p in pubkeys: + xfp_paths.append(p.get_full_derivation_int_list(0)) + pk = p.get_pubkey_bytes(0) + redeem_script += len(pk).to_bytes(1, byteorder="little") + pk - xfp_paths.append(str_to_int_path(xfp, p)) + redeem_script += (80 + len(pubkeys)).to_bytes(1, byteorder="little") + redeem_script += b"\xae" - payload = CCProtocolPacker.show_p2sh_address(min_signers, xfp_paths, script, addr_fmt=addr_fmt) - # single-sig - else: - payload = CCProtocolPacker.show_address(keypath, addr_fmt=addr_fmt) + payload = CCProtocolPacker.show_p2sh_address(threshold, xfp_paths, redeem_script, addr_fmt=addr_fmt) address = self.device.send_recv(payload, timeout=None) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index c8e827a0a..6d9559fda 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -13,8 +13,13 @@ import socket import sys import time -from typing import Dict, Union +from typing import ( + Dict, + List, + Union, +) +from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -547,7 +552,10 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st return {"signature": base64.b64encode(compact_sig).decode('utf-8')} # Display address of specified type on the device. - def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): + def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> Dict[str, str]: + raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') + + def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> Dict[str, str]: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') # Setup a new device diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 92f7da401..754e81c47 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,7 +1,12 @@ # Ledger interaction script -from typing import Dict, Union +from typing import ( + Dict, + List, + Union, +) +from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -347,16 +352,27 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. Only supports single-key based addresses. @ledger_exception - def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): + def display_singlesig_address( + self, + keypath: str, + addr_type: AddressType, + ) -> Dict[str, str]: if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") - if redeem_script is not None: - raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") p2sh_p2wpkh = addr_type == AddressType.SH_WPKH bech32 = addr_type == AddressType.WPKH output = self.app.getWalletPublicKey(keypath[2:], True, p2sh_p2wpkh or bech32, bech32) return {'address': output['address'][12:-2]} # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. + @ledger_exception + def display_multisig_address( + self, + threshold: int, + pubkeys: List[PubkeyProvider], + addr_type: AddressType, + ) -> Dict[str, str]: + raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") + # Setup a new device def setup_device(self, label='', passphrase=''): raise UnavailableActionError('The Ledger Nano S and X do not support software setup') diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 25898b344..f4cd12703 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,7 +1,11 @@ # Trezor interaction script -from typing import Dict, Union - +from typing import ( + Dict, + List, + Union, +) +from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..errors import ( ActionCanceledError, @@ -416,55 +420,81 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Display address of specified type on the device. @trezor_exception - def display_address(self, keypath, addr_type: AddressType, redeem_script=None, descriptor=None): + def display_singlesig_address( + self, + keypath: str, + addr_type: AddressType, + ) -> Dict[str, str]: self._check_unlocked() - # descriptor means multisig with xpubs - if descriptor: - pubkeys = [] - for p in descriptor.pubkeys: - xpub = ExtendedKey.deserialize(p.pubkey) - hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) - pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) - multisig = proto.MultisigRedeemScriptType(m=descriptor.thresh, signatures=[b''] * len(descriptor.pubkeys), pubkeys=pubkeys) - # redeem_script means p2sh/multisig - elif redeem_script: - # Get multisig object required by Trezor's get_address - multisig = parse_multisig(bytes.fromhex(redeem_script)) - if not multisig[0]: - raise BadArgumentError("The redeem script provided is not a multisig. Only multisig scripts can be displayed.") - multisig = multisig[1] - else: - multisig = None - # Script type if addr_type == AddressType.SH_WPKH: script_type = proto.InputScriptType.SPENDP2SHWITNESS elif addr_type == AddressType.WPKH: script_type = proto.InputScriptType.SPENDWITNESS - elif redeem_script: - script_type = proto.InputScriptType.SPENDMULTISIG else: script_type = proto.InputScriptType.SPENDADDRESS - # convert device fingerprint to 'm' if exists in path - keypath = keypath.replace(self.get_master_fingerprint_hex(), 'm') + expanded_path = parse_path(keypath) - for path in keypath.split(','): - if len(path.split('/')[0]) == 8: - path = path.split('/', 1)[1] - expanded_path = parse_path(path) + try: + address = btc.get_address( + self.client, + self.coin_name, + expanded_path, + show_display=True, + script_type=script_type, + multisig=None, + ) + return {'address': address} + except Exception: + pass + + raise BadArgumentError("No path supplied matched device keys") + + @trezor_exception + def display_multisig_address( + self, + threshold: int, + pubkeys: List[PubkeyProvider], + addr_type: AddressType + ) -> Dict[str, str]: + self._check_unlocked() + + pubkey_objs = [] + for p in pubkeys: + if p.extkey is not None: + xpub = p.extkey + hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) + pubkey_objs.append(proto.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) + else: + hd_node = proto.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=p.get_pubkey_bytes(0)) + pubkey_objs.append(proto.HDNodePathType(node=hd_node, address_n=[])) + + multisig = proto.MultisigRedeemScriptType(m=threshold, signatures=[b''] * len(pubkey_objs), pubkeys=pubkey_objs) + + # Script type + if addr_type == AddressType.SH_WPKH: + script_type = proto.InputScriptType.SPENDP2SHWITNESS + elif addr_type == AddressType.WPKH: + script_type = proto.InputScriptType.SPENDWITNESS + else: + script_type = proto.InputScriptType.SPENDMULTISIG + for p in pubkeys: + keypath = p.origin.get_derivation_path() if p.origin is not None else "m/" + keypath += p.deriv_path if p.deriv_path is not None else "" + path = parse_path(keypath) try: address = btc.get_address( self.client, self.coin_name, - expanded_path, + path, show_display=True, script_type=script_type, multisig=multisig, ) - return {'address': address} + return {"address": address} except Exception: pass diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 78aa4bbeb..083893d8e 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -1,7 +1,11 @@ -from typing import Dict, Optional, Union - +from typing import ( + Dict, + List, + Optional, + Union, +) from .base58 import get_xpub_fingerprint_hex -from .descriptor import Descriptor +from .descriptor import PubkeyProvider from .serializations import AddressType, PSBT @@ -72,16 +76,12 @@ def sign_message( raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def display_address( + def display_singlesig_address( self, bip32_path: str, addr_type: AddressType, - redeem_script: Optional[str] = None, - descriptor: Optional[Descriptor] = None, ) -> Dict[str, str]: - """Display and return the address of specified type. - - redeem_script is a hex-string. + """Display and return the single sig address of specified type. Retrieve the public key at the specified BIP32 derivation path. @@ -90,6 +90,19 @@ def display_address( raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") + def display_multisig_address( + self, + threshold: int, + pubkeys: List[PubkeyProvider], + addr_type: AddressType, + ) -> Dict[str, str]: + """Display and return the multisig address of specified type given the threshold and pubkeys. + + Return {"address": }. + """ + raise NotImplementedError("The HardwareWalletClient base class " + "does not implement this method") + def wipe_device(self) -> Dict[str, Union[bool, str, int]]: """Wipe the HID device. diff --git a/hwilib/key.py b/hwilib/key.py index 385b51686..a1d12094b 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -12,6 +12,7 @@ import struct from typing import ( Dict, + List, Optional, Sequence, Tuple, @@ -253,6 +254,15 @@ def get_fingerprint_hex(self) -> str: """ return binascii.hexlify(self.fingerprint).decode() + def get_full_int_list(self) -> List[int]: + """ + Return a list of ints representing this KeyOriginInfo. + The first int is the fingerprint, followed by the path + """ + xfp = [struct.unpack(" Sequence[int]: """ diff --git a/test/test_device.py b/test/test_device.py index 127685201..5f9cee89c 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -570,29 +570,22 @@ def test_display_address_multisig(self): raise unittest.SkipTest("{} does not support multisig display".format(self.full_type)) for addrtype in ["pkh", "sh_wpkh", "wpkh"]: - for use_desc in [True, False]: - with self.subTest(addrtype=addrtype, use_desc=use_desc): - addr, desc, rs, path = self._make_single_multisig(addrtype) - - if use_desc: - args = ['displayaddress', '--desc', desc] - else: - args = ['displayaddress', '--path', path, '--redeem_script', rs] - if addrtype != "pkh": - args.append("--addr-type") - args.append(addrtype) - - result = self.do_command(self.dev_args + args) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) - - if addrtype == "wpkh": - # removes prefix and checksum since regtest gives - # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix - self.assertEqual(addr[4:58], result['address'][2:56]) - else: - self.assertEqual(addr, result['address']) + with self.subTest(addrtype=addrtype): + addr, desc, rs, path = self._make_single_multisig(addrtype) + + args = ['displayaddress', '--desc', desc] + + result = self.do_command(self.dev_args + args) + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + + if addrtype == "wpkh": + # removes prefix and checksum since regtest gives + # prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix + self.assertEqual(addr[4:58], result['address'][2:56]) + else: + self.assertEqual(addr, result['address']) def test_display_address_xpub_multisig(self): if self.full_type not in SUPPORTS_XPUB_MS_DISPLAY: From ef66c4f34742209f9d345d8a28c62892b53ab82f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Feb 2021 13:47:42 -0500 Subject: [PATCH 257/634] gui: Use address type in getkeypool and displayaddress getkeypool an displayaddress use address type now so we need to do this in the gui --- hwilib/gui.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/hwilib/gui.py b/hwilib/gui.py index afb6c8328..b92768e11 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -9,6 +9,7 @@ from . import commands, __version__ from .cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED +from .serializations import AddressType try: from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog @@ -170,7 +171,12 @@ def __init__(self, client): @Slot() def go_button_clicked(self): path = self.ui.path_lineedit.text() - res = do_command(commands.displayaddress, self.client, path, sh_wpkh=self.ui.sh_wpkh_radio.isChecked(), wpkh=self.ui.wpkh_radio.isChecked()) + addrtype = AddressType.PKH + if self.ui.sh_wpkh_radio.isChecked(): + addrtype = AddressType.SH_WPKH + elif self.ui.wpkh_radio.isChecked(): + addrtype = AddressType.WPKH + res = do_command(commands.displayaddress, self.client, path, addr_type=addrtype) self.ui.address_lineedit.setText(res['address']) class GetKeypoolOptionsDialog(QDialog): @@ -198,8 +204,9 @@ def __init__(self, opts): self.ui.path_lineedit.setEnabled(True) self.ui.account_spinbox.setEnabled(False) self.ui.path_lineedit.setText(opts['path']) - self.ui.sh_wpkh_radio.setChecked(opts['sh_wpkh']) - self.ui.wpkh_radio.setChecked(opts['wpkh']) + self.ui.sh_wpkh_radio.setChecked(opts['addrtype'] == AddressType.SH_WPKH) + self.ui.wpkh_radio.setChecked(opts['addrtype'] == AddressType.WPKH) + self.ui.pkh_radio.setChecked(opts['addrtype'] == AddressType.PKH) self.ui.account_radio.toggled.connect(self.toggle_account) @@ -275,8 +282,7 @@ def __init__(self, passphrase='', testnet=False): 'account': 0, 'internal': False, 'keypool': True, - 'sh_wpkh': True, - 'wpkh': False, + 'addrtype': AddressType.SH_WPKH, 'path': None, 'account_used': True } @@ -382,8 +388,7 @@ def get_device_info(self): self.getkeypool_opts['internal'], self.getkeypool_opts['keypool'], self.getkeypool_opts['account'], - self.getkeypool_opts['sh_wpkh'], - self.getkeypool_opts['wpkh']) + self.getkeypool_opts['addrtype']) descriptors = do_command(commands.getdescriptors, self.client, self.getkeypool_opts['account']) self.ui.keypool_textedit.setPlainText(json.dumps(keypool, indent=2)) @@ -435,8 +440,11 @@ def getkeypooloptionsdialog_accepted(self): self.getkeypool_opts['end'] = self.current_dialog.ui.end_spinbox.value() self.getkeypool_opts['internal'] = self.current_dialog.ui.internal_checkbox.isChecked() self.getkeypool_opts['keypool'] = self.current_dialog.ui.keypool_checkbox.isChecked() - self.getkeypool_opts['sh_wpkh'] = self.current_dialog.ui.sh_wpkh_radio.isChecked() - self.getkeypool_opts['wpkh'] = self.current_dialog.ui.wpkh_radio.isChecked() + self.getkeypool_opts['addrtype'] = AddressType.PKH + if self.current_dialog.ui.sh_wpkh_radio.isChecked(): + self.getkeypool_opts['addrtype'] = AddressType.SH_WPKH + if self.current_dialog.ui.wpkh_radio.isChecked(): + self.getkeypool_opts['addrtype'] = AddressType.WPKH if self.current_dialog.ui.account_radio.isChecked(): self.getkeypool_opts['account'] = self.current_dialog.ui.account_spinbox.value() self.getkeypool_opts['account_used'] = True From 3b76baf49da4a7abda36600853836412e6193c82 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Feb 2021 15:02:23 -0500 Subject: [PATCH 258/634] Update btchip to latest from btchip-python --- hwilib/devices/btchip/__init__.py | 3 +- hwilib/devices/btchip/bitcoinTransaction.py | 4 +- hwilib/devices/btchip/btchip.py | 317 +++++++++++++++++++- hwilib/devices/btchip/btchipComm.py | 77 ++++- 4 files changed, 390 insertions(+), 11 deletions(-) diff --git a/hwilib/devices/btchip/__init__.py b/hwilib/devices/btchip/__init__.py index 7e1b55eaa..2e30a630d 100644 --- a/hwilib/devices/btchip/__init__.py +++ b/hwilib/devices/btchip/__init__.py @@ -16,4 +16,5 @@ * limitations under the License. ******************************************************************************** """ -__version__ = "0.1.30" +__version__ = "0.1.31" + diff --git a/hwilib/devices/btchip/bitcoinTransaction.py b/hwilib/devices/btchip/bitcoinTransaction.py index 9543c7abc..35276e9e1 100644 --- a/hwilib/devices/btchip/bitcoinTransaction.py +++ b/hwilib/devices/btchip/bitcoinTransaction.py @@ -101,14 +101,14 @@ def __init__(self, data=None): inputSize = readVarint(data, offset) offset += inputSize['size'] numInputs = inputSize['value'] - for _ in range(numInputs): + for i in range(numInputs): tmp = { 'buffer': data, 'offset' : offset} self.inputs.append(bitcoinInput(tmp)) offset = tmp['offset'] outputSize = readVarint(data, offset) offset += outputSize['size'] numOutputs = outputSize['value'] - for _ in range(numOutputs): + for i in range(numOutputs): tmp = { 'buffer': data, 'offset' : offset} self.outputs.append(bitcoinOutput(tmp)) offset = tmp['offset'] diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index 9b14bbc55..262f4b9a0 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -22,6 +22,7 @@ from .bitcoinVarint import * from .btchipException import * from .btchipHelpers import * +from .btchipKeyRecovery import * from binascii import hexlify, unhexlify class btchip: @@ -85,7 +86,33 @@ def __init__(self, dongle): else: self.scriptBlockLength = 255 except Exception: - pass + pass + try: + result = self.getJCExtendedFeatures() + self.needKeyCache = (result['proprietaryApi'] == False) + except Exception: + pass + + def setAlternateCoinVersion(self, versionRegular, versionP2SH): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_ALTERNATE_COIN_VERSION, 0x00, 0x00, 0x02, versionRegular, versionP2SH] + self.dongle.exchange(bytearray(apdu)) + + def verifyPin(self, pin): + if isinstance(pin, str): + pin = pin.encode('utf-8') + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x00, 0x00, len(pin) ] + apdu.extend(bytearray(pin)) + self.dongle.exchange(bytearray(apdu)) + + def getVerifyPinRemainingAttempts(self): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x80, 0x00, 0x01 ] + apdu.extend(bytearray(b'0')) + try: + self.dongle.exchange(bytearray(apdu)) + except BTChipException as e: + if ((e.sw & 0xfff0) == 0x63c0): + return e.sw - 0x63c0 + raise e def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): result = {} @@ -233,9 +260,9 @@ def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, rede self.dongle.exchange(bytearray(apdu)) offset += blockLength if len(script) == 0: - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] - apdu.extend(sequence) - self.dongle.exchange(bytearray(apdu)) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] + apdu.extend(sequence) + self.dongle.exchange(bytearray(apdu)) currentIndex += 1 def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): @@ -305,6 +332,47 @@ def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): result['outputData'] = outputs return result + def finalizeInputFull(self, outputData): + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(outputData)): + blockLength = self.scriptBlockLength + if ((offset + blockLength) < len(outputData)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputData) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputData[offset : offset + dataLength]) + response = self.dongle.exchange(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += dataLength + if len(response) > 1: + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + else: + # Support for old style API before 1.0.2 + result['confirmationNeeded'] = response[0] != 0x00 + result['confirmationType'] = response[0] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] # legacy + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + return result + def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01): if isinstance(pin, str): pin = pin.encode('utf-8') @@ -324,6 +392,27 @@ def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01): result[0] = 0x30 return result + def signMessagePrepareV1(self, path, message): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, 0x00 ] + params = [] + params.extend(donglePath) + params.append(len(message)) + params.extend(bytearray(message)) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['confirmationNeeded'] = response[0] != 0x00 + result['confirmationType'] = response[0] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1:] + if result['confirmationType'] == 0x03: + result['secureScreenData'] = response[1:] + return result + def signMessagePrepareV2(self, path, message): donglePath = parse_bip32_path(path) if self.needKeyCache: @@ -386,6 +475,87 @@ def signMessageSign(self, pin=""): response = self.dongle.exchange(bytearray(apdu)) return response + def setup(self, operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH, userPin, wipePin, keymapEncoding, seed=None, developerKey=None): + if isinstance(userPin, str): + userPin = userPin.encode('utf-8') + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SETUP, 0x00, 0x00 ] + params = [ operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH ] + params.append(len(userPin)) + params.extend(bytearray(userPin)) + if wipePin is not None: + if isinstance(wipePin, str): + wipePin = wipePin.encode('utf-8') + params.append(len(wipePin)) + params.extend(bytearray(wipePin)) + else: + params.append(0x00) + if seed is not None: + if len(seed) < 32 or len(seed) > 64: + raise BTChipException("Invalid seed length") + params.append(len(seed)) + params.extend(seed) + else: + params.append(0x00) + if developerKey is not None: + params.append(len(developerKey)) + params.extend(developerKey) + else: + params.append(0x00) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['trustedInputKey'] = response[0:16] + result['developerKey'] = response[16:] + self.setKeymapEncoding(keymapEncoding) + try: + self.setTypingBehaviour(0xff, 0xff, 0xff, 0x10) + except BTChipException as e: + if (e.sw == 0x6700): # Old firmware version, command not supported + pass + else: + raise + return result + + def setKeymapEncoding(self, keymapEncoding): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x00, 0x00 ] + apdu.append(len(keymapEncoding)) + apdu.extend(keymapEncoding) + self.dongle.exchange(bytearray(apdu)) + + def setTypingBehaviour(self, unitDelayStart, delayStart, unitDelayKey, delayKey): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x01, 0x00 ] + params = [] + writeUint32BE(unitDelayStart, params) + writeUint32BE(delayStart, params) + writeUint32BE(unitDelayKey, params) + writeUint32BE(delayKey, params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + + def getOperationMode(self): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_OPERATION_MODE, 0x00, 0x00, 0x00] + response = self.dongle.exchange(bytearray(apdu)) + return response[0] + + def setOperationMode(self, operationMode): + if operationMode != btchip.OPERATION_MODE_WALLET \ + and operationMode != btchip.OPERATION_MODE_RELAXED_WALLET \ + and operationMode != btchip.OPERATION_MODE_SERVER \ + and operationMode != btchip.OPERATION_MODE_DEVELOPER: + raise BTChipException("Invalid operation mode") + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, 0x00, 0x00, 0x01, operationMode ] + self.dongle.exchange(bytearray(apdu)) + + def enableAlternate2fa(self, persistent): + if persistent: + p1 = 0x02 + else: + p1 = 0x01 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, p1, 0x00, 0x01, btchip.OPERATION_MODE_WALLET ] + self.dongle.exchange(bytearray(apdu)) + def getFirmwareVersion(self): result = {} apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] @@ -399,8 +569,141 @@ def getFirmwareVersion(self): raise result['compressedKeys'] = (response[0] == 0x01) result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) - result['major_version'] = response[2] - result['minor_version'] = response[3] - result['patch_version'] = response[4] result['specialVersion'] = response[1] return result + + def getRandom(self, size): + if size > 255: + raise BTChipException("Invalid size") + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_RANDOM, 0x00, 0x00, size ] + return self.dongle.exchange(bytearray(apdu)) + + def getPOSSeedKey(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x01, 0x00, 0x00 ] + return self.dongle.exchange(bytearray(apdu)) + + def getPOSEncryptedSeed(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x02, 0x00, 0x00 ] + return self.dongle.exchange(bytearray(apdu)) + + def importPrivateKey(self, data, isSeed=False): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_IMPORT_PRIVATE_KEY, (0x02 if isSeed else 0x01), 0x00 ] + apdu.append(len(data)) + apdu.extend(data) + return self.dongle.exchange(bytearray(apdu)) + + def getPublicKey(self, encodedPrivateKey): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(encodedPrivateKey) + 1) + apdu.append(len(encodedPrivateKey)) + apdu.extend(encodedPrivateKey) + response = self.dongle.exchange(bytearray(apdu)) + offset = 1 + result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] + offset = offset + 1 + response[offset] + if response[0] == 0x02: + result['chainCode'] = response[offset : offset + 32] + offset = offset + 32 + result['depth'] = response[offset] + offset = offset + 1 + result['parentFingerprint'] = response[offset : offset + 4] + offset = offset + 4 + result['childNumber'] = response[offset : offset + 4] + return result + + def deriveBip32Key(self, encodedPrivateKey, path): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + offset = 1 + currentEncodedPrivateKey = encodedPrivateKey + while (offset < len(donglePath)): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_DERIVE_BIP32_KEY, 0x00, 0x00 ] + apdu.append(len(currentEncodedPrivateKey) + 1 + 4) + apdu.append(len(currentEncodedPrivateKey)) + apdu.extend(currentEncodedPrivateKey) + apdu.extend(donglePath[offset : offset + 4]) + currentEncodedPrivateKey = self.dongle.exchange(bytearray(apdu)) + offset = offset + 4 + return currentEncodedPrivateKey + + def signImmediate(self, encodedPrivateKey, data, deterministic=True): + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGNVERIFY_IMMEDIATE, 0x00, (0x80 if deterministic else 0x00) ] + apdu.append(len(encodedPrivateKey) + len(data) + 2); + apdu.append(len(encodedPrivateKey)) + apdu.extend(encodedPrivateKey) + apdu.append(len(data)) + apdu.extend(data) + return self.dongle.exchange(bytearray(apdu)) + +# Functions dedicated to the Java Card interface when no proprietary API is available + + def parse_bip32_path_internal(self, path): + if len(path) == 0: + return [] + result = [] + elements = path.split('/') + for pathElement in elements: + element = pathElement.split('\'') + if len(element) == 1: + result.append(int(element[0])) + else: + result.append(0x80000000 | int(element[0])) + return result + + def serialize_bip32_path_internal(self, path): + result = [] + for pathElement in path: + writeUint32BE(pathElement, result) + return bytearray([ len(path) ] + result) + + def resolvePublicKey(self, path): + expandedPath = self.serialize_bip32_path_internal(path) + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath)) + apdu.extend(expandedPath) + result = self.dongle.exchange(bytearray(apdu)) + if (result[0] == 0): + # Not present, need to be inserted into the cache + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath)) + apdu.extend(expandedPath) + result = self.dongle.exchange(bytearray(apdu)) + hashData = result[0:32] + keyX = result[32:64] + signature = result[64:] + keyXY = recoverKey(signature, hashData, keyX) + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY, 0x00, 0x00 ] + apdu.append(len(expandedPath) + 65) + apdu.extend(expandedPath) + apdu.extend(keyXY) + self.dongle.exchange(bytearray(apdu)) + + def resolvePublicKeysInPath(self, path): + splitPath = self.parse_bip32_path_internal(path) + # Locate the first public key in path + offset = 0 + startOffset = 0 + while(offset < len(splitPath)): + if (splitPath[offset] < 0x80000000): + startOffset = offset + break + offset = offset + 1 + if startOffset != 0: + searchPath = splitPath[0:startOffset - 1] + offset = startOffset - 1 + while(offset < len(splitPath)): + searchPath = searchPath + [ splitPath[offset] ] + self.resolvePublicKey(searchPath) + offset = offset + 1 + self.resolvePublicKey(splitPath) + + def getJCExtendedFeatures(self): + result = {} + apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_GET_FEATURES, 0x00, 0x00, 0x00 ] + response = self.dongle.exchange(bytearray(apdu)) + result['proprietaryApi'] = ((response[0] & 0x01) != 0) + return result diff --git a/hwilib/devices/btchip/btchipComm.py b/hwilib/devices/btchip/btchipComm.py index 6e23140d9..3a5fe0010 100644 --- a/hwilib/devices/btchip/btchipComm.py +++ b/hwilib/devices/btchip/btchipComm.py @@ -146,6 +146,33 @@ def close(self): pass self.opened = False +class DongleSmartcard(Dongle): + + def __init__(self, device, debug=False): + self.device = device + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) + sw = (sw1 << 8) | sw2 + if self.debug: + print("<= %s%.2x" % (toHexString(response).replace(" ", ""), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return bytearray(response) + + def close(self): + if self.opened: + try: + self.device.disconnect() + except Exception: + pass + self.opened = False + class DongleServer(Dongle): def __init__(self, server, port, debug=False): @@ -160,7 +187,7 @@ def __init__(self, server, port, debug=False): def exchange(self, apdu, timeout=20000): if self.debug: - print("=> %s" % hexlify(apdu)) + print("=> %s" % hexlify(apdu)) self.socket.send(struct.pack(">I", len(apdu))) self.socket.send(apdu) size = struct.unpack(">I", self.socket.recv(4))[0] @@ -177,3 +204,51 @@ def close(self): self.socket.close() except Exception: pass + +def getDongle(debug=False): + dev = None + hidDevicePath = None + ledger = False + if HID: + for hidDevice in hid.enumerate(0, 0): + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x2b7c: + hidDevicePath = hidDevice['path'] + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x3b7c: + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x4b7c: + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2c97: + if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0): + hidDevicePath = hidDevice['path'] + ledger = True + if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x1807: + hidDevicePath = hidDevice['path'] + if hidDevicePath is not None: + dev = hid.device() + dev.open_path(hidDevicePath) + dev.set_nonblocking(True) + return HIDDongleHIDAPI(dev, ledger, debug) + + if SCARD: + connection = None + for reader in readers(): + try: + connection = reader.createConnection() + connection.connect() + response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031")) + sw = (sw1 << 8) | sw2 + if sw == 0x9000: + break + else: + connection.disconnect() + connection = None + except Exception: + connection = None + pass + if connection is not None: + return DongleSmartcard(connection, debug) + if (os.getenv("LEDGER_PROXY_ADDRESS") is not None) and (os.getenv("LEDGER_PROXY_PORT") is not None): + return DongleServer(os.getenv("LEDGER_PROXY_ADDRESS"), int(os.getenv("LEDGER_PROXY_PORT")), debug) + raise BTChipException("No dongle found") From c141b6effa78fb7a3ed52acbe17314078fe93c86 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Feb 2021 15:30:14 -0500 Subject: [PATCH 259/634] Modify btchip for our usage --- hwilib/devices/btchip/btchip.py | 288 +--------------------------- hwilib/devices/btchip/btchipComm.py | 75 -------- 2 files changed, 3 insertions(+), 360 deletions(-) diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py index 262f4b9a0..0648c881c 100644 --- a/hwilib/devices/btchip/btchip.py +++ b/hwilib/devices/btchip/btchip.py @@ -22,7 +22,6 @@ from .bitcoinVarint import * from .btchipException import * from .btchipHelpers import * -from .btchipKeyRecovery import * from binascii import hexlify, unhexlify class btchip: @@ -87,32 +86,6 @@ def __init__(self, dongle): self.scriptBlockLength = 255 except Exception: pass - try: - result = self.getJCExtendedFeatures() - self.needKeyCache = (result['proprietaryApi'] == False) - except Exception: - pass - - def setAlternateCoinVersion(self, versionRegular, versionP2SH): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_ALTERNATE_COIN_VERSION, 0x00, 0x00, 0x02, versionRegular, versionP2SH] - self.dongle.exchange(bytearray(apdu)) - - def verifyPin(self, pin): - if isinstance(pin, str): - pin = pin.encode('utf-8') - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x00, 0x00, len(pin) ] - apdu.extend(bytearray(pin)) - self.dongle.exchange(bytearray(apdu)) - - def getVerifyPinRemainingAttempts(self): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_VERIFY_PIN, 0x80, 0x00, 0x01 ] - apdu.extend(bytearray(b'0')) - try: - self.dongle.exchange(bytearray(apdu)) - except BTChipException as e: - if ((e.sw & 0xfff0) == 0x63c0): - return e.sw - 0x63c0 - raise e def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): result = {} @@ -332,47 +305,6 @@ def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): result['outputData'] = outputs return result - def finalizeInputFull(self, outputData): - result = {} - offset = 0 - encryptedOutputData = b"" - while (offset < len(outputData)): - blockLength = self.scriptBlockLength - if ((offset + blockLength) < len(outputData)): - dataLength = blockLength - p1 = 0x00 - else: - dataLength = len(outputData) - offset - p1 = 0x80 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ - p1, 0x00, dataLength ] - apdu.extend(outputData[offset : offset + dataLength]) - response = self.dongle.exchange(bytearray(apdu)) - encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] - offset += dataLength - if len(response) > 1: - result['confirmationNeeded'] = response[1 + response[0]] != 0x00 - result['confirmationType'] = response[1 + response[0]] - else: - # Support for old style API before 1.0.2 - result['confirmationNeeded'] = response[0] != 0x00 - result['confirmationType'] = response[0] - if result['confirmationType'] == 0x02: - result['keycardData'] = response[1 + response[0] + 1:] # legacy - if result['confirmationType'] == 0x03: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - offset = offset + 1 - result['keycardData'] = response[offset : offset + keycardDataLength] - offset = offset + keycardDataLength - result['secureScreenData'] = response[offset:] - result['encryptedOutputData'] = encryptedOutputData - if result['confirmationType'] == 0x04: - offset = 1 + response[0] + 1 - keycardDataLength = response[offset] - result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] - return result - def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01): if isinstance(pin, str): pin = pin.encode('utf-8') @@ -475,87 +407,6 @@ def signMessageSign(self, pin=""): response = self.dongle.exchange(bytearray(apdu)) return response - def setup(self, operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH, userPin, wipePin, keymapEncoding, seed=None, developerKey=None): - if isinstance(userPin, str): - userPin = userPin.encode('utf-8') - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SETUP, 0x00, 0x00 ] - params = [ operationModeFlags, featuresFlag, keyVersion, keyVersionP2SH ] - params.append(len(userPin)) - params.extend(bytearray(userPin)) - if wipePin is not None: - if isinstance(wipePin, str): - wipePin = wipePin.encode('utf-8') - params.append(len(wipePin)) - params.extend(bytearray(wipePin)) - else: - params.append(0x00) - if seed is not None: - if len(seed) < 32 or len(seed) > 64: - raise BTChipException("Invalid seed length") - params.append(len(seed)) - params.extend(seed) - else: - params.append(0x00) - if developerKey is not None: - params.append(len(developerKey)) - params.extend(developerKey) - else: - params.append(0x00) - apdu.append(len(params)) - apdu.extend(params) - response = self.dongle.exchange(bytearray(apdu)) - result['trustedInputKey'] = response[0:16] - result['developerKey'] = response[16:] - self.setKeymapEncoding(keymapEncoding) - try: - self.setTypingBehaviour(0xff, 0xff, 0xff, 0x10) - except BTChipException as e: - if (e.sw == 0x6700): # Old firmware version, command not supported - pass - else: - raise - return result - - def setKeymapEncoding(self, keymapEncoding): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x00, 0x00 ] - apdu.append(len(keymapEncoding)) - apdu.extend(keymapEncoding) - self.dongle.exchange(bytearray(apdu)) - - def setTypingBehaviour(self, unitDelayStart, delayStart, unitDelayKey, delayKey): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_KEYMAP, 0x01, 0x00 ] - params = [] - writeUint32BE(unitDelayStart, params) - writeUint32BE(delayStart, params) - writeUint32BE(unitDelayKey, params) - writeUint32BE(delayKey, params) - apdu.append(len(params)) - apdu.extend(params) - self.dongle.exchange(bytearray(apdu)) - - def getOperationMode(self): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_OPERATION_MODE, 0x00, 0x00, 0x00] - response = self.dongle.exchange(bytearray(apdu)) - return response[0] - - def setOperationMode(self, operationMode): - if operationMode != btchip.OPERATION_MODE_WALLET \ - and operationMode != btchip.OPERATION_MODE_RELAXED_WALLET \ - and operationMode != btchip.OPERATION_MODE_SERVER \ - and operationMode != btchip.OPERATION_MODE_DEVELOPER: - raise BTChipException("Invalid operation mode") - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, 0x00, 0x00, 0x01, operationMode ] - self.dongle.exchange(bytearray(apdu)) - - def enableAlternate2fa(self, persistent): - if persistent: - p1 = 0x02 - else: - p1 = 0x01 - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SET_OPERATION_MODE, p1, 0x00, 0x01, btchip.OPERATION_MODE_WALLET ] - self.dongle.exchange(bytearray(apdu)) - def getFirmwareVersion(self): result = {} apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] @@ -569,141 +420,8 @@ def getFirmwareVersion(self): raise result['compressedKeys'] = (response[0] == 0x01) result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) + result['major_version'] = response[2] + result['minor_version'] = response[3] + result['patch_version'] = response[4] result['specialVersion'] = response[1] return result - - def getRandom(self, size): - if size > 255: - raise BTChipException("Invalid size") - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_RANDOM, 0x00, 0x00, size ] - return self.dongle.exchange(bytearray(apdu)) - - def getPOSSeedKey(self): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x01, 0x00, 0x00 ] - return self.dongle.exchange(bytearray(apdu)) - - def getPOSEncryptedSeed(self): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_POS_SEED, 0x02, 0x00, 0x00 ] - return self.dongle.exchange(bytearray(apdu)) - - def importPrivateKey(self, data, isSeed=False): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_IMPORT_PRIVATE_KEY, (0x02 if isSeed else 0x01), 0x00 ] - apdu.append(len(data)) - apdu.extend(data) - return self.dongle.exchange(bytearray(apdu)) - - def getPublicKey(self, encodedPrivateKey): - result = {} - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(encodedPrivateKey) + 1) - apdu.append(len(encodedPrivateKey)) - apdu.extend(encodedPrivateKey) - response = self.dongle.exchange(bytearray(apdu)) - offset = 1 - result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] - offset = offset + 1 + response[offset] - if response[0] == 0x02: - result['chainCode'] = response[offset : offset + 32] - offset = offset + 32 - result['depth'] = response[offset] - offset = offset + 1 - result['parentFingerprint'] = response[offset : offset + 4] - offset = offset + 4 - result['childNumber'] = response[offset : offset + 4] - return result - - def deriveBip32Key(self, encodedPrivateKey, path): - donglePath = parse_bip32_path(path) - if self.needKeyCache: - self.resolvePublicKeysInPath(path) - offset = 1 - currentEncodedPrivateKey = encodedPrivateKey - while (offset < len(donglePath)): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_DERIVE_BIP32_KEY, 0x00, 0x00 ] - apdu.append(len(currentEncodedPrivateKey) + 1 + 4) - apdu.append(len(currentEncodedPrivateKey)) - apdu.extend(currentEncodedPrivateKey) - apdu.extend(donglePath[offset : offset + 4]) - currentEncodedPrivateKey = self.dongle.exchange(bytearray(apdu)) - offset = offset + 4 - return currentEncodedPrivateKey - - def signImmediate(self, encodedPrivateKey, data, deterministic=True): - apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGNVERIFY_IMMEDIATE, 0x00, (0x80 if deterministic else 0x00) ] - apdu.append(len(encodedPrivateKey) + len(data) + 2); - apdu.append(len(encodedPrivateKey)) - apdu.extend(encodedPrivateKey) - apdu.append(len(data)) - apdu.extend(data) - return self.dongle.exchange(bytearray(apdu)) - -# Functions dedicated to the Java Card interface when no proprietary API is available - - def parse_bip32_path_internal(self, path): - if len(path) == 0: - return [] - result = [] - elements = path.split('/') - for pathElement in elements: - element = pathElement.split('\'') - if len(element) == 1: - result.append(int(element[0])) - else: - result.append(0x80000000 | int(element[0])) - return result - - def serialize_bip32_path_internal(self, path): - result = [] - for pathElement in path: - writeUint32BE(pathElement, result) - return bytearray([ len(path) ] + result) - - def resolvePublicKey(self, path): - expandedPath = self.serialize_bip32_path_internal(path) - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath)) - apdu.extend(expandedPath) - result = self.dongle.exchange(bytearray(apdu)) - if (result[0] == 0): - # Not present, need to be inserted into the cache - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath)) - apdu.extend(expandedPath) - result = self.dongle.exchange(bytearray(apdu)) - hashData = result[0:32] - keyX = result[32:64] - signature = result[64:] - keyXY = recoverKey(signature, hashData, keyX) - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY, 0x00, 0x00 ] - apdu.append(len(expandedPath) + 65) - apdu.extend(expandedPath) - apdu.extend(keyXY) - self.dongle.exchange(bytearray(apdu)) - - def resolvePublicKeysInPath(self, path): - splitPath = self.parse_bip32_path_internal(path) - # Locate the first public key in path - offset = 0 - startOffset = 0 - while(offset < len(splitPath)): - if (splitPath[offset] < 0x80000000): - startOffset = offset - break - offset = offset + 1 - if startOffset != 0: - searchPath = splitPath[0:startOffset - 1] - offset = startOffset - 1 - while(offset < len(splitPath)): - searchPath = searchPath + [ splitPath[offset] ] - self.resolvePublicKey(searchPath) - offset = offset + 1 - self.resolvePublicKey(splitPath) - - def getJCExtendedFeatures(self): - result = {} - apdu = [ self.BTCHIP_JC_EXT_CLA, self.BTCHIP_INS_EXT_CACHE_GET_FEATURES, 0x00, 0x00, 0x00 ] - response = self.dongle.exchange(bytearray(apdu)) - result['proprietaryApi'] = ((response[0] & 0x01) != 0) - return result diff --git a/hwilib/devices/btchip/btchipComm.py b/hwilib/devices/btchip/btchipComm.py index 3a5fe0010..012fbb891 100644 --- a/hwilib/devices/btchip/btchipComm.py +++ b/hwilib/devices/btchip/btchipComm.py @@ -146,33 +146,6 @@ def close(self): pass self.opened = False -class DongleSmartcard(Dongle): - - def __init__(self, device, debug=False): - self.device = device - self.debug = debug - self.waitImpl = self - self.opened = True - - def exchange(self, apdu, timeout=20000): - if self.debug: - print("=> %s" % hexlify(apdu)) - response, sw1, sw2 = self.device.transmit(toBytes(hexlify(apdu))) - sw = (sw1 << 8) | sw2 - if self.debug: - print("<= %s%.2x" % (toHexString(response).replace(" ", ""), sw)) - if sw != 0x9000: - raise BTChipException("Invalid status %04x" % sw, sw) - return bytearray(response) - - def close(self): - if self.opened: - try: - self.device.disconnect() - except Exception: - pass - self.opened = False - class DongleServer(Dongle): def __init__(self, server, port, debug=False): @@ -204,51 +177,3 @@ def close(self): self.socket.close() except Exception: pass - -def getDongle(debug=False): - dev = None - hidDevicePath = None - ledger = False - if HID: - for hidDevice in hid.enumerate(0, 0): - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x2b7c: - hidDevicePath = hidDevice['path'] - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x3b7c: - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x4b7c: - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2c97: - if ('interface_number' in hidDevice and hidDevice['interface_number'] == 0) or ('usage_page' in hidDevice and hidDevice['usage_page'] == 0xffa0): - hidDevicePath = hidDevice['path'] - ledger = True - if hidDevice['vendor_id'] == 0x2581 and hidDevice['product_id'] == 0x1807: - hidDevicePath = hidDevice['path'] - if hidDevicePath is not None: - dev = hid.device() - dev.open_path(hidDevicePath) - dev.set_nonblocking(True) - return HIDDongleHIDAPI(dev, ledger, debug) - - if SCARD: - connection = None - for reader in readers(): - try: - connection = reader.createConnection() - connection.connect() - response, sw1, sw2 = connection.transmit(toBytes("00A4040010FF4C4547522E57414C5430312E493031")) - sw = (sw1 << 8) | sw2 - if sw == 0x9000: - break - else: - connection.disconnect() - connection = None - except Exception: - connection = None - pass - if connection is not None: - return DongleSmartcard(connection, debug) - if (os.getenv("LEDGER_PROXY_ADDRESS") is not None) and (os.getenv("LEDGER_PROXY_PORT") is not None): - return DongleServer(os.getenv("LEDGER_PROXY_ADDRESS"), int(os.getenv("LEDGER_PROXY_PORT")), debug) - raise BTChipException("No dongle found") From 3e771958cc811caf22b1f1c4c77008764a09a534 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 4 Feb 2021 15:31:14 -0500 Subject: [PATCH 260/634] Update btchip readme --- hwilib/devices/btchip/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hwilib/devices/btchip/README.md b/hwilib/devices/btchip/README.md index c2eeb3f1f..43c7d7053 100644 --- a/hwilib/devices/btchip/README.md +++ b/hwilib/devices/btchip/README.md @@ -2,8 +2,10 @@ This is a stripped down and modified version of the official [btchip-python](https://github.com/LedgerHQ/btchip-python) library. -This stripped down version was made at commit [fe82d7f5638169f583a445b8e200fd1c9f3ea218](https://github.com/LedgerHQ/btchip-python/tree/fe82d7f5638169f583a445b8e200fd1c9f3ea218). +This stripped down version was made at commit [17f27c1996c75145b8eb5d16583bddcb6e2bf691](https://github.com/LedgerHQ/btchip-python/tree/17f27c1996c75145b8eb5d16583bddcb6e2bf691). ## Changes - Removed support for Ledger HW.1 and other unused things + +See c141b6effa78fb7a3ed52acbe17314078fe93c86 for the specific changes. From aa837144bb06357ddb09b2d2000f7bfd8f1126f7 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 29 Jan 2021 15:48:05 +0100 Subject: [PATCH 261/634] Ledger: fix change path check --- hwilib/devices/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 754e81c47..396135b87 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -217,7 +217,7 @@ def sign_tx(self, tx): # For possible matches, check if pubkey matches possible template if hash160(pubkey) in txout.scriptPubKey or hash160(bytearray.fromhex("0014") + hash160(pubkey)) in txout.scriptPubKey: change_path = '' - for index in origin.path[1:]: + for index in origin.path: change_path += str(index) + "/" change_path = change_path[:-1] From c8fd9fecaf3a25e71b355d567019e7705f7b6102 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 5 Feb 2021 13:49:05 +0100 Subject: [PATCH 262/634] Bump Python patch version to 3.6.12 Except for Wine --- .python-version | 2 +- contrib/build.Dockerfile | 4 ++-- contrib/build_bin.sh | 2 +- contrib/build_wine.sh | 1 + docs/release-process.md | 8 ++++---- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.python-version b/.python-version index 424e1794d..8b7b0b52e 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.6.8 +3.6.12 diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index eb92a943e..9d02265f7 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -36,9 +36,9 @@ COPY contrib/reproducible-python.diff /opt/reproducible-python.diff ENV PYTHON_CONFIGURE_OPTS="--enable-shared" ENV BUILD_DATE="Jan 1 2019" ENV BUILD_TIME="00:00:00" -RUN eval "$(pyenv init -)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.6.8 +RUN eval "$(pyenv init -)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.6.12 -RUN dpkg --add-architecture i386 +RUN dpkg --add-architecture i386 RUN wget -nc https://dl.winehq.org/wine-builds/winehq.key RUN apt-key add winehq.key RUN echo "deb https://dl.winehq.org/wine-builds/debian/ stretch main" >> /etc/apt/sources.list diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 00147a3bb..01818e367 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -12,7 +12,7 @@ pip install poetry poetry install -E qt # We also need to change the timestamps of all of the base library files -lib_dir=`pyenv root`/versions/3.6.8/lib/python3.6 +lib_dir=`pyenv root`/versions/3.6.12/lib/python3.6 TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; # Make the standalone binary diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 7b48b204e..9dbb2de77 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -3,6 +3,7 @@ set -ex +# No Windows installer for 3.6.12 PYTHON_VERSION=3.6.8 PYTHON_FOLDER="python3" diff --git a/docs/release-process.md b/docs/release-process.md index 626012999..ce8d14ccc 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -27,17 +27,17 @@ Note that the macOS build is non-deterministic. First install [pyenv](https://github.com/pyenv/pyenv) using whichever method you prefer. -Then a deterministic build of Python 3.6.8 needs to be installed. This can be done with the patch in `contrib/reproducible-python.diff`. First `cd` into HWI's source tree. Then use: +Then a deterministic build of Python 3.6.12 needs to be installed. This can be done with the patch in `contrib/reproducible-python.diff`. First `cd` into HWI's source tree. Then use: ``` -cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8 +cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.12 ``` -Make sure that python 3.6.8 is active +Make sure that python 3.6.12 is active ``` $ python --version -Python 3.6.8 +Python 3.6.12 ``` Now install [Poetry](https://github.com/sdispater/poetry) with `pip install poetry` From 59b4edf917a7ce79f219992d3b299ac0c84f14a1 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 10 Nov 2020 21:52:39 +0100 Subject: [PATCH 263/634] PSBT: also parse and serialize PSBT_GLOBAL_XPUB Needed for BitBox02 to check and perform the xpubs registration. --- hwilib/serializations.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 990f3669f..975d8c411 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -521,9 +521,10 @@ def DeserializeHDKeypath( f: Readable, key: bytes, hd_keypaths: MutableMapping[bytes, KeyOriginInfo], + expected_sizes: Sequence[int], ) -> None: - if len(key) != 34 and len(key) != 66: - raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") + if len(key) not in expected_sizes: + raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey. Length: {}".format(len(key))) pubkey = key[1:] if pubkey in hd_keypaths: raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") @@ -630,7 +631,7 @@ def deserialize(self, f: Readable) -> None: self.witness_script = deser_string(f) elif key_type == 6: - DeserializeHDKeypath(f, key, self.hd_keypaths) + DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66]) elif key_type == 7: if len(self.final_script_sig) != 0: @@ -745,7 +746,7 @@ def deserialize(self, f: Readable) -> None: self.witness_script = deser_string(f) elif key_type == 2: - DeserializeHDKeypath(f, key, self.hd_keypaths) + DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66]) else: if key in self.unknown: @@ -783,6 +784,7 @@ def __init__(self, tx: Optional[CTransaction] = None) -> None: self.inputs: List[PartiallySignedInput] = [] self.outputs: List[PartiallySignedOutput] = [] self.unknown: Dict[bytes, bytes] = {} + self.xpub: Dict[bytes, KeyOriginInfo] = {} def deserialize(self, psbt: str) -> None: psbt_bytes = base64.b64decode(psbt.strip()) @@ -825,7 +827,8 @@ def deserialize(self, psbt: str) -> None: for txin in self.tx.vin: if len(txin.scriptSig) != 0 or not self.tx.wit.is_null(): raise PSBTSerializationError("Unsigned tx does not have empty scriptSigs and scriptWitnesses") - + elif key_type == 0x01: + DeserializeHDKeypath(f, key, self.xpub, [79]) else: if key in self.unknown: raise PSBTSerializationError("Duplicate key, key for unknown value already provided") @@ -877,6 +880,9 @@ def serialize(self) -> str: r += ser_compact_size(len(tx)) r += tx + # write xpubs + r += SerializeHDKeypath(self.xpub, b"\x01") + # unknowns for key, value in sorted(self.unknown.items()): r += ser_string(key) From d3d9fc93418533afe92984e94264a4f6140f011c Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sun, 7 Feb 2021 16:10:29 +0100 Subject: [PATCH 264/634] test/data/test_psbt: add tests for PSBT_GLOBAL_XPUB Valid test contains 2 xpubs. Invalid test contains 2 xpubs, one of which is too short (not 78 bytes as expected). --- test/data/test_psbt.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/data/test_psbt.json b/test/data/test_psbt.json index 39dccdbca..1286bdd4b 100644 --- a/test/data/test_psbt.json +++ b/test/data/test_psbt.json @@ -17,7 +17,8 @@ "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A", "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A", - "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" + "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A", + "cHNidP8BAHMCAAAAAY43bdygsmO9x9a2AyzUpshOgpVx6re/d/vuYAfvGEfIAQAAAAD9////AvgRAAAAAAAAF6kUA9NxbxOrP4pij9OIuyjnY/dAp4mHMP0TAAAAAAAXqRS59fWkR2DcQ+z1w7bOwilZXcDwFIfz6BwATgECQonvBBKS8Q2AAAABsd4yUFWH7gQx+vPwZENJfFff0NAzfMTVnhtoUnv62W0CeZO1Ujk7zdBOIWWQgJS+fJISyuy5qfSr9f9gxRCgaxQZVC6wMAAAgAEAAIAAAACAAQAAgE8BAkKJ7wRppKYYgAAAAXa9y9oj7zcS7452oc2h4ZNxRhq/QTLlKS560A1atY4fAnASZtL9Rsu9GJjX80COOwGkUYCLPpLFnjpzogzw1Ma9FOgaV0QwAACAAQAAgAAAAIABAACAAAEA9wIAAAAAAQHizx3xPxzn6CVTzYoRvZ6glR+I5NCz3EUfy2KosxPV6gAAAAAXFgAUPjTLr1dUB4jjlBZT/oesgrjIXbf+////AmrMfggBAAAAF6kUhI2CBLIHFddfYSZ40rXWFajy2LqHPxAUAAAAAAAXqRT0k6FCcz5GDJmyHUiK4sGv60T7XIcCRzBEAiB5tPi/H34vGKryr5oqITYpRpnrxuOBv2FPwsdnUD9nmQIgQD47b/bMepSLpin3KNzTxNJifEcu+B6XV+L4xwWBgeQBIQOzL6gF2ObchppgdS08ShOuOJOdevhWJonswr02kL0WQ/PoHAAiAgP3DH3/zjXEaELmksr3MxkKoG63lA+KdIJUrSu9S7rz3EcwRAIgAuMNHJ/F1oiUSy/TZcmKUyxkjHfZ4FcafzaYl1FL66wCIApaVukx8bCYartDMOHlwn99Mq4hYOx8lvXEzNVc6NyIAQEEIgAgtUUwTMw06MhQH9f9ltFa1H2OoZOByEzcQSNlUHvjlIUBBUdSIQOin1plRrlkmYGt/MCIFdz2yh3yg5ATXZKXThCaPZNMJyED9wx9/841xGhC5pLK9zMZCqBut5QPinSCVK0rvUu689xSriIGA6KfWmVGuWSZga38wIgV3PbKHfKDkBNdkpdOEJo9k0wnHOgaV0QwAACAAQAAgAAAAIABAACAAAAAAAAAAAAiBgP3DH3/zjXEaELmksr3MxkKoG63lA+KdIJUrSu9S7rz3BwZVC6wMAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAAAEAIgAgBfoZdJ+MY8g0ZypFFXPF9yQpYIJMRqQ+T0JA5OPfKWkBAUdSIQKPQaPd43GOhCOoprKnaAeE+jB+MXTu9GU5rXGhfFBVfSECm2qtddhDhdeQz3rmBk3VNi2e6U5oWX+Dz+HQDS7GXNJSriICAo9Bo93jcY6EI6imsqdoB4T6MH4xdO70ZTmtcaF8UFV9HBlULrAwAACAAQAAgAAAAIABAACAAQAAAAAAAAAiAgKbaq112EOF15DPeuYGTdU2LZ7pTmhZf4PP4dANLsZc0hzoGldEMAAAgAEAAIAAAACAAQAAgAEAAAAAAAAAAAEAIgAgbgVZZTDEmbGsvV/lW1JP9KDQaH+EZFRo8yboIGMjCGQBAUdSIQMjZva1A9ooUCsZsKCi9LkJkliAjk+KiZoDM40/JE6x6yEDy3BsaBCpLLwNmkaNlHfmIl6sBS5oZ3YMMqKXJD0VNPhSriICAyNm9rUD2ihQKxmwoKL0uQmSWICOT4qJmgMzjT8kTrHrHBlULrAwAACAAQAAgAAAAIABAACAAAAAAAEAAAAiAgPLcGxoEKksvA2aRo2Ud+YiXqwFLmhndgwyopckPRU0+BzoGldEMAAAgAEAAIAAAACAAQAAgAAAAAABAAAAAA==" ], "valid" : [ "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA", @@ -26,7 +27,8 @@ "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" ], "creator" : [ { From 1835d3c54a3d4654398314c2de99922e40f08832 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 10 Nov 2020 22:42:58 +0100 Subject: [PATCH 265/634] serializations: add parse_multisig It is adapted from the function of the same name in trezor.py. It will be used in BitBox02 for multisig, and can also be re-used by trezor.py. --- hwilib/serializations.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 975d8c411..13b6e5020 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -323,6 +323,35 @@ def is_p2wsh(script: bytes) -> bool: return False return len(wit_prog) == 32 +# Only handles up to 15 of 15. Returns None if this script is not a +# multisig script. Returns (m, pubkeys) otherwise. +def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: + # Get m + m = script[0] - 80 + if m < 1 or m > 15: + return None + + # Get pubkeys + pubkeys = [] + offset = 1 + while True: + pubkey_len = script[offset] + if pubkey_len != 33: + break + offset += 1 + pubkeys.append(script[offset:offset + 33]) + offset += 33 + + # Check things at the end + n = script[offset] - 80 + if n != len(pubkeys): + return None + offset += 1 + op_cms = script[offset] + if op_cms != 174: + return None + + return (m, pubkeys) class CTxOut(object): def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): From 10b8b0d1549466c08b0abe72c58bc7830d68edc8 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 10 Nov 2020 23:36:43 +0100 Subject: [PATCH 266/634] bitbox02: restructure signing code Add script configs on the fly, let the BitBox02 validate them. --- hwilib/devices/bitbox02.py | 81 +++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 8982defac..172ccbd2b 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -137,6 +137,13 @@ def attestation_check(self, result: bool) -> None: ) +def _keypath_hardened_prefix(keypath: Sequence[int]) -> Sequence[int]: + for i, e in builtins.enumerate(keypath): + if e & HARDENED == 0: + return keypath[:i] + return keypath + + def enumerate(password: str = "") -> List[Dict[str, object]]: """ Enumerate all BitBox02 devices. Bootloaders excluded. @@ -383,17 +390,46 @@ def find_our_key( return pubkey, origin.path return None, None - def get_simple_type( - output: CTxOut, redeem_script: bytes - ) -> bitbox02.btc.BTCScriptConfig.SimpleType: + script_configs: List[bitbox02.btc.BTCScriptConfigWithKeypath] = [] + + def add_script_config( + script_config: bitbox02.btc.BTCScriptConfigWithKeypath + ) -> int: + # Find index of script config if already added. + script_config_index = next( + ( + i + for i, e in builtins.enumerate(script_configs) + if e.SerializeToString() == script_config.SerializeToString() + ), + None, + ) + if script_config_index is not None: + return script_config_index + script_configs.append(script_config) + return len(script_configs) - 1 + + def script_config_from_utxo( + output: CTxOut, keypath: Sequence[int], redeem_script: bytes + ) -> bitbox02.btc.BTCScriptConfigWithKeypath: if is_p2pkh(output.scriptPubKey): raise BadArgumentError( "The BitBox02 does not support legacy p2pkh scripts" ) if is_p2wpkh(output.scriptPubKey): - return bitbox02.btc.BTCScriptConfig.P2WPKH + return bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ), + keypath=_keypath_hardened_prefix(keypath), + ) if output.is_p2sh() and is_p2wpkh(redeem_script): - return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + return bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ), + keypath=_keypath_hardened_prefix(keypath), + ) raise BadArgumentError( "Input script type not recognized of input {}.".format(input_index) ) @@ -454,8 +490,6 @@ def get_simple_type( assert keypath is not None found_pubkeys.append(found_pubkey) - # TOOD: validate keypath - if bip44_account is None: bip44_account = keypath[2] elif bip44_account != keypath[2]: @@ -463,13 +497,9 @@ def get_simple_type( "The bip44 account index must be the same for all inputs and changes" ) - simple_type = get_simple_type(utxo, psbt_in.redeem_script) - - script_config_index_map = { - bitbox02.btc.BTCScriptConfig.P2WPKH: 0, - bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH: 1, - } - + script_config_index = add_script_config( + script_config_from_utxo(utxo, keypath, psbt_in.redeem_script) + ) inputs.append( { "prev_out_hash": ser_uint256(tx_in.prevout.hash), @@ -477,7 +507,7 @@ def get_simple_type( "prev_out_value": utxo.nValue, "sequence": tx_in.nSequence, "keypath": keypath, - "script_config_index": script_config_index_map[simple_type], + "script_config_index": script_config_index, "prev_tx": { "version": prevtx.nVersion, "locktime": prevtx.nLockTime, @@ -509,12 +539,14 @@ def get_simple_type( is_change = keypath and keypath[-2] == 1 if is_change: assert keypath is not None - simple_type = get_simple_type(tx_out, psbt_out.redeem_script) + script_config_index = add_script_config( + script_config_from_utxo(tx_out, keypath, psbt_out.redeem_script) + ) outputs.append( bitbox02.BTCOutputInternal( keypath=keypath, value=tx_out.nValue, - script_config_index=script_config_index_map[simple_type], + script_config_index=script_config_index, ) ) else: @@ -548,20 +580,7 @@ def get_simple_type( bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED sigs = self.init().btc_sign( bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC, - [ - bitbox02.btc.BTCScriptConfigWithKeypath( - script_config=bitbox02.btc.BTCScriptConfig( - simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH - ), - keypath=[84 + HARDENED, bip44_network, bip44_account], - ), - bitbox02.btc.BTCScriptConfigWithKeypath( - script_config=bitbox02.btc.BTCScriptConfig( - simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH - ), - keypath=[49 + HARDENED, bip44_network, bip44_account], - ), - ], + script_configs, inputs=inputs, outputs=outputs, locktime=psbt.tx.nLockTime, From f530a5fa10e9a7e4734dde62c4b350c1bb30e590 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 11 Nov 2020 01:05:08 +0100 Subject: [PATCH 267/634] bitbox02: implement multisig signing If the multisig setup is not yet registered on the device, the user is prompted to register and verify the setup on the device. --- hwilib/devices/bitbox02.py | 119 ++++++++++++++++++++++++++++++++++--- hwilib/serializations.py | 3 + 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 172ccbd2b..cb758a173 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -4,6 +4,7 @@ Callable, Dict, Optional, + Mapping, Union, Tuple, List, @@ -14,6 +15,8 @@ import sys from functools import wraps +import base58 + from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..serializations import ( @@ -25,6 +28,7 @@ is_p2wsh, ser_uint256, ser_sig_der, + parse_multisig, ) from ..errors import ( HWWError, @@ -144,6 +148,15 @@ def _keypath_hardened_prefix(keypath: Sequence[int]) -> Sequence[int]: return keypath +def _xpubs_equal_ignoring_version(xpub1: bytes, xpub2: bytes) -> bool: + """ + Xpubs: 78 bytes. Returns true if the xpubs are equal, ignoring the 4 byte version. + The version is not important and allows compatibility with Electrum, which exports PSBTs with + xpubs using Electrum-style xpub versions. + """ + return xpub1[4:] == xpub2[4:] + + def enumerate(password: str = "") -> List[Dict[str, object]]: """ Enumerate all BitBox02 devices. Bootloaders excluded. @@ -334,6 +347,69 @@ def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: raise BitBox02Error(str(exc)) return {"xpub": xpub} + def _maybe_register_script_config( + self, script_config: bitbox02.btc.BTCScriptConfig, keypath: Sequence[int] + ) -> None: + bb02 = self.init() + is_registered = bb02.btc_is_script_config_registered( + self._get_coin(), script_config, keypath + ) + if not is_registered: + bb02.btc_register_script_config( + coin=self._get_coin(), + script_config=script_config, + keypath=keypath, + name="", # enter name on the device + xpub_type=bitbox02.btc.BTCRegisterScriptConfigRequest.AUTO_XPUB_TPUB, + ) + + def _multisig_scriptconfig( + self, + threshold: int, + origin_infos: Mapping[bytes, KeyOriginInfo], + script_type: bitbox02.btc.BTCScriptConfig.Multisig.ScriptType, + ) -> Tuple[str, bitbox02.btc.BTCScriptConfigWithKeypath]: + """ + From a threshold, {xpub: KeyOriginInfo} mapping and multisig script type, + return our xpub and the BitBox02 multisig script config. + """ + # Figure out which of the cosigners is us. + device_fingerprint = self.get_master_fingerprint() + our_xpub_index = None + our_account_keypath = None + + xpubs: List[str] = [] + for i, (xpub, keyinfo) in builtins.enumerate(origin_infos.items()): + xpubs.append(base58.b58encode_check(xpub).decode()) + if device_fingerprint == keyinfo.fingerprint and keyinfo.path: + if _xpubs_equal_ignoring_version( + base58.b58decode_check(self._get_xpub(keyinfo.path)), xpub + ): + our_xpub_index = i + our_account_keypath = keyinfo.path + + if our_xpub_index is None: + raise BadArgumentError("This BitBox02 is not one of the cosigners") + assert our_account_keypath + + if len(xpubs) != len(set(xpubs)): + raise BadArgumentError("Duplicate xpubs not supported") + + return ( + xpubs[our_xpub_index], + bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=bitbox02.btc.BTCScriptConfig( + multisig=bitbox02.btc.BTCScriptConfig.Multisig( + threshold=threshold, + xpubs=map(util.parse_xpub, xpubs), + our_xpub_index=our_xpub_index, + script_type=script_type, + ) + ), + keypath=our_account_keypath, + ), + ) + @bitbox02_exception def display_singlesig_address( self, @@ -410,7 +486,10 @@ def add_script_config( return len(script_configs) - 1 def script_config_from_utxo( - output: CTxOut, keypath: Sequence[int], redeem_script: bytes + output: CTxOut, + keypath: Sequence[int], + redeem_script: bytes, + witness_script: bytes, ) -> bitbox02.btc.BTCScriptConfigWithKeypath: if is_p2pkh(output.scriptPubKey): raise BadArgumentError( @@ -430,9 +509,26 @@ def script_config_from_utxo( ), keypath=_keypath_hardened_prefix(keypath), ) - raise BadArgumentError( - "Input script type not recognized of input {}.".format(input_index) - ) + # Check for segwit multisig (p2wsh or p2wsh-p2sh). + is_p2wsh_p2sh = output.is_p2sh() and is_p2wsh(redeem_script) + if output.is_p2wsh() or is_p2wsh_p2sh: + multisig = parse_multisig(witness_script) + if multisig: + threshold, _ = multisig + # We assume that all xpubs in the PSBT are part of the multisig. This is okay + # since the BitBox02 enforces the same script type for all inputs and + # changes. If that should change, we need to find and use the subset of xpubs + # corresponding to the public keys in the current multisig script. + _, script_config = self._multisig_scriptconfig( + threshold, + psbt.xpub, + bitbox02.btc.BTCScriptConfig.Multisig.P2WSH + if output.is_p2wsh() + else bitbox02.btc.BTCScriptConfig.Multisig.P2WSH_P2SH, + ) + return script_config + + raise BadArgumentError("Input or change script type not recognized.") master_fp = self.get_master_fingerprint() @@ -498,7 +594,9 @@ def script_config_from_utxo( ) script_config_index = add_script_config( - script_config_from_utxo(utxo, keypath, psbt_in.redeem_script) + script_config_from_utxo( + utxo, keypath, psbt_in.redeem_script, psbt_in.witness_script + ) ) inputs.append( { @@ -540,7 +638,9 @@ def script_config_from_utxo( if is_change: assert keypath is not None script_config_index = add_script_config( - script_config_from_utxo(tx_out, keypath, psbt_out.redeem_script) + script_config_from_utxo( + tx_out, keypath, psbt_out.redeem_script, psbt_out.witness_script + ) ) outputs.append( bitbox02.BTCOutputInternal( @@ -576,10 +676,13 @@ def script_config_from_utxo( ) assert bip44_account is not None + if len(script_configs) == 1 and script_configs[0].script_config.multisig: + self._maybe_register_script_config( + script_configs[0].script_config, script_configs[0].keypath + ) - bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED sigs = self.init().btc_sign( - bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC, + self._get_coin(), script_configs, inputs=inputs, outputs=outputs, diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 13b6e5020..838078127 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -374,6 +374,9 @@ def is_opreturn(self) -> bool: def is_p2sh(self) -> bool: return is_p2sh(self.scriptPubKey) + def is_p2wsh(self) -> bool: + return is_p2wsh(self.scriptPubKey) + def is_p2pkh(self) -> bool: return is_p2pkh(self.scriptPubKey) From 55ae05ae5d3a72673d98f4cd5b5ffd6ab8ac283f Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 25 Nov 2020 16:48:41 +0100 Subject: [PATCH 268/634] bitbox02: update docs --- README.md | 4 ++-- docs/bitbox02.md | 9 ++++----- hwilib/devices/bitbox02.py | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9505035db..059b130dc 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ Please also see [docs](docs/) for additional information about each device. | P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | | P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | -| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | +| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | | Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | diff --git a/docs/bitbox02.md b/docs/bitbox02.md index 6949234dd..9e951933a 100644 --- a/docs/bitbox02.md +++ b/docs/bitbox02.md @@ -13,20 +13,19 @@ Current implemented commands are: * `backup` * `togglepassphrase` -Multisig (P2WSH only) is supported by the BitBox02, but is not ingerated into HWI yet. Coming -soon^{tm}. - # Usage Notes ## Strict keypaths The BitBox02 has strict keypath validation. -The only accepted keypaths for xpubs are: +The only accepted keypaths for xpubs are (as of firmware v9.4.0): - `m/49'/0'/` for `p2wpkh-p2sh` (segwit wrapped in P2SH) - `m/84'/0'/` for `p2wpkh` (native segwit v0) -- `m/48'/0'//2` for p2wsh multisig (native segwit v0 multisig). +- `m/48'/0'//2'` for p2wsh multisig (native segwit v0 multisig). +- `m/48'/0'//1'` for p2wsh-p2sh multisig (p2sh-wrapped segwit v0 multisig). +- `m/48'/0'/` for p2wsh and p2wsh-p2sh multisig. `account'` can be between `0'` and `99'`. diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index cb758a173..293baecb0 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -78,6 +78,8 @@ def __init__(self, msg: str): msg += "m/49'/0'/ for p2wpkh-p2sh; " msg += "m/84'/0'/ for p2wpkh; " msg += "m/48'/0'//2' for p2wsh multisig; " + msg += "m/48'/0'//1' for p2wsh-p2sh multisig; " + msg += "m/48'/0'/' for any supported multisig; " msg += "account can be between 0' and 99'; " msg += "For address keypaths, append /0/
for a receive and /1/ for a change address." super().__init__(msg) From be2f95a9e2eddb67125fb19baa396bce14aa698c Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Tue, 9 Feb 2021 19:33:13 +0100 Subject: [PATCH 269/634] Introduce --chain command line argument (replacing --testnet) --- docs/bitbox02.md | 2 +- docs/bitcoin-core-usage.md | 2 +- hwilib/cli.py | 7 +++++-- hwilib/gui.py | 7 +++++-- test/test_device.py | 2 +- test/test_digitalbitbox.py | 3 ++- test/test_ledger.py | 3 ++- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/bitbox02.md b/docs/bitbox02.md index 6949234dd..5ad260996 100644 --- a/docs/bitbox02.md +++ b/docs/bitbox02.md @@ -33,7 +33,7 @@ The only accepted keypaths for xpubs are: For address keypaths, append `/0/
` for a receive and `/1/` for a change address. Up to `10000` addresses are supported. -In `--testnet` mode, the second element must be `1'` (e.g. `m/49'/1'/...`). +In `--chain test` mode, the second element must be `1'` (e.g. `m/49'/1'/...`). ## Signing with mixed input types diff --git a/docs/bitcoin-core-usage.md b/docs/bitcoin-core-usage.md index 608fdc762..2d90e9256 100644 --- a/docs/bitcoin-core-usage.md +++ b/docs/bitcoin-core-usage.md @@ -250,7 +250,7 @@ Once the transaction has been inspected and everything looks good, the transacti ``` $ cd ../HWI -$ ./hwi.py -f 8038ecd9 --testnet signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA +$ ./hwi.py -f 8038ecd9 --chain test signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA ``` Follow the onscreen instructions, check everything, and approve the transaction. The result will look like: diff --git a/hwilib/cli.py b/hwilib/cli.py index 61d03d7fb..131d6302c 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -117,12 +117,14 @@ def error(self, message): self.exit(2) def process_commands(cli_args): + CHAINS = ['main', 'test', 'regtest', 'signet'] + parser = HWIArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__)) parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to') parser.add_argument('--device-type', '-t', help='Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.') parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') parser.add_argument('--stdinpass', help='Enter the device password on the command line', action='store_true') - parser.add_argument('--testnet', help='Use testnet prefixes', action='store_true') + parser.add_argument('--chain', help='Select chain to work with ({})'.format(', '.join(CHAINS)), default='main', choices=CHAINS) parser.add_argument('--debug', help='Print debug statements', action='store_true') parser.add_argument('--fingerprint', '-f', help='Specify the device to connect to using the first 4 bytes of the hash160 of the master public key. It will connect to the first device that matches this fingerprint.') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) @@ -265,7 +267,8 @@ def process_commands(cli_args): else: return {'error': 'You must specify a device type or fingerprint for all commands except enumerate', 'code': NO_DEVICE_TYPE} - client.is_testnet = args.testnet + client.chain = args.chain + client.is_testnet = args.chain in ['test', 'regtest', 'signet'] # Do the commands with handle_errors(result=result, debug=args.debug): diff --git a/hwilib/gui.py b/hwilib/gui.py index b92768e11..40e2c0f37 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -461,9 +461,11 @@ def toggle_passphrase(self): self.show_sendpindialog(prompt_pin=False) def process_gui_commands(cli_args): + CHAINS = ['main', 'test', 'regtest', 'signet'] + parser = HWIArgumentParser(description='Hardware Wallet Interface Qt, version {}.\nInteractively access and send commands to a hardware wallet device with a GUI. Responses are in JSON format.'.format(__version__)) parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') - parser.add_argument('--testnet', help='Use testnet prefixes', action='store_true') + parser.add_argument('--chain', help='Select chain to work with ({})'.format(', '.join(CHAINS)), default='main', choices=CHAINS) parser.add_argument('--debug', help='Print debug statements', action='store_true') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) @@ -478,7 +480,8 @@ def process_gui_commands(cli_args): # Qt setup app = QApplication() - window = HWIQt(args.password, args.testnet) + is_testnet = args.chain in ['test', 'regtest', 'signet'] + window = HWIQt(args.password, is_testnet) window.refresh_clicked() diff --git a/test/test_device.py b/test/test_device.py index 5f9cee89c..caf6e7ad3 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -75,7 +75,7 @@ def __init__(self, rpc, rpc_userpass, type, full_type, path, fingerprint, master self.fingerprint = fingerprint self.master_xpub = master_xpub self.password = password - self.dev_args = ['-t', self.type, '-d', self.path, '--testnet'] + self.dev_args = ['-t', self.type, '-d', self.path, '--chain', 'test'] if emulator: self.emulator = emulator else: diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index b7e1a7961..e64d7da03 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -134,7 +134,8 @@ def test_backup(self): class TestBitboxGetXpub(DeviceTestCase): def setUp(self): - self.dev_args.remove('--testnet') + self.dev_args.remove('--chain') + self.dev_args.remove('test') def test_getxpub(self): result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) diff --git a/test/test_ledger.py b/test/test_ledger.py index 815952123..cdddfe0a0 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -102,7 +102,8 @@ def test_backup(self): class TestLedgerGetXpub(DeviceTestCase): def setUp(self): - self.dev_args.remove("--testnet") + self.dev_args.remove("--chain") + self.dev_args.remove("test") def test_getxpub(self): result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/44h/0h/0h/3']) From a7d14ba40611749685901581ac9feddb8e2f8ecd Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Wed, 10 Feb 2021 17:29:08 +0100 Subject: [PATCH 270/634] Remove client.is_testnet usage in getdescriptor --- hwilib/commands.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index f2b1fbce3..904f7ac7c 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -115,7 +115,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc except NotImplementedError as e: return {'error': str(e), 'code': NOT_IMPLEMENTED} - desc = getdescriptor(client, master_fpr, client.is_testnet, path, internal, addr_type, account, start, end) + desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) if not isinstance(desc, Descriptor): return desc @@ -131,9 +131,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import['watchonly'] = True return [this_import] -def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, addr_type=AddressType.WPKH, account=0, start=None, end=None): - testnet = client.is_testnet - +def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=AddressType.WPKH, account=0, start=None, end=None): is_wpkh = addr_type is AddressType.WPKH is_sh_wpkh = addr_type is AddressType.SH_WPKH @@ -149,10 +147,10 @@ def getdescriptor(client, master_fpr, testnet=False, path=None, internal=False, parsed_path.append(H_(44)) # Coin type - if testnet: - parsed_path.append(H_(1)) - else: + if client.chain == 'main': parsed_path.append(H_(0)) + else: + parsed_path.append(H_(1)) # Account parsed_path.append(H_(account)) @@ -233,7 +231,7 @@ def getdescriptors(client, account=0): descriptors = [] for addr_type in (AddressType.PKH, AddressType.SH_WPKH, AddressType.WPKH): try: - desc = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=addr_type, account=account) + desc = getdescriptor(client, master_fpr=master_fpr, internal=internal, addr_type=addr_type, account=account) except UnavailableActionError: # Device does not support this address type or network. Skip. continue From 36f3e4a962103de469dcb4ca201c02bc9541d1f5 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 1 Feb 2021 17:59:30 -0500 Subject: [PATCH 271/634] Update the rest of trezorlib Updates trezorlib. Definitely does not work and trezor.py will need changes, maybe some to trezorlib too. --- hwilib/devices/keepkey.py | 5 - hwilib/devices/trezor.py | 5 - hwilib/devices/trezorlib/README.md | 5 +- hwilib/devices/trezorlib/__init__.py | 7 +- hwilib/devices/trezorlib/btc.py | 222 ++++++- hwilib/devices/trezorlib/client.py | 322 +++++++--- hwilib/devices/trezorlib/debuglink.py | 579 +++++++++++------- hwilib/devices/trezorlib/device.py | 168 ++--- hwilib/devices/trezorlib/exceptions.py | 11 +- hwilib/devices/trezorlib/firmware.py | 405 ++++++++---- hwilib/devices/trezorlib/log.py | 21 +- hwilib/devices/trezorlib/mapping.py | 24 +- hwilib/devices/trezorlib/messages/Address.py | 14 +- .../devices/trezorlib/messages/AmountUnit.py | 12 + .../devices/trezorlib/messages/ApplyFlags.py | 12 +- .../trezorlib/messages/ApplySettings.py | 31 +- .../trezorlib/messages/AuthorizeCoinJoin.py | 48 ++ .../trezorlib/messages/BackupDevice.py | 7 + .../devices/trezorlib/messages/BackupType.py | 11 + .../devices/trezorlib/messages/ButtonAck.py | 7 + .../trezorlib/messages/ButtonRequest.py | 18 +- .../trezorlib/messages/ButtonRequestType.py | 41 +- hwilib/devices/trezorlib/messages/Cancel.py | 7 + .../devices/trezorlib/messages/Capability.py | 25 + .../devices/trezorlib/messages/ChangePin.py | 12 +- .../trezorlib/messages/ClearSession.py | 7 - .../trezorlib/messages/DebugLinkDecision.py | 30 +- .../messages/DebugLinkEraseSdCard.py | 27 + .../trezorlib/messages/DebugLinkFlashErase.py | 12 +- .../trezorlib/messages/DebugLinkGetState.py | 26 + .../trezorlib/messages/DebugLinkLog.py | 25 - .../trezorlib/messages/DebugLinkMemory.py | 12 +- .../trezorlib/messages/DebugLinkMemoryRead.py | 14 +- .../messages/DebugLinkMemoryWrite.py | 16 +- .../messages/DebugLinkRecordScreen.py | 27 + .../messages/DebugLinkReseedRandom.py | 27 + .../trezorlib/messages/DebugLinkShowText.py | 38 ++ .../messages/DebugLinkShowTextItem.py | 30 + .../messages/DebugLinkShowTextStyle.py | 14 + .../trezorlib/messages/DebugLinkState.py | 46 +- .../trezorlib/messages/DebugLinkStop.py | 7 + .../messages/DebugLinkWatchLayout.py | 27 + .../trezorlib/messages/DebugSwipeDirection.py | 12 + .../messages/Deprecated_PassphraseStateAck.py | 7 + .../Deprecated_PassphraseStateRequest.py | 16 +- .../trezorlib/messages/DoPreauthorized.py | 14 + .../devices/trezorlib/messages/EndSession.py | 14 + hwilib/devices/trezorlib/messages/Entropy.py | 14 +- .../devices/trezorlib/messages/EntropyAck.py | 12 +- .../trezorlib/messages/EntropyRequest.py | 7 + hwilib/devices/trezorlib/messages/Failure.py | 17 +- .../devices/trezorlib/messages/FailureType.py | 34 +- hwilib/devices/trezorlib/messages/Features.py | 106 +++- .../trezorlib/messages/FirmwareErase.py | 12 +- .../trezorlib/messages/FirmwareRequest.py | 14 +- .../trezorlib/messages/FirmwareUpload.py | 16 +- .../devices/trezorlib/messages/GetAddress.py | 24 +- .../devices/trezorlib/messages/GetEntropy.py | 14 +- .../devices/trezorlib/messages/GetFeatures.py | 7 + .../trezorlib/messages/GetOwnershipId.py | 39 ++ .../trezorlib/messages/GetOwnershipProof.py | 48 ++ .../trezorlib/messages/GetPublicKey.py | 24 +- .../trezorlib/messages/HDNodePathType.py | 14 +- .../devices/trezorlib/messages/HDNodeType.py | 34 +- .../trezorlib/messages/IdentityType.py | 33 - .../devices/trezorlib/messages/Initialize.py | 19 +- .../trezorlib/messages/InputScriptType.py | 16 +- .../devices/trezorlib/messages/LoadDevice.py | 39 +- .../devices/trezorlib/messages/LockDevice.py | 14 + .../trezorlib/messages/MessageSignature.py | 14 +- .../devices/trezorlib/messages/MessageType.py | 126 ++-- .../messages/MultisigRedeemScriptType.py | 19 +- .../trezorlib/messages/OutputScriptType.py | 18 +- .../devices/trezorlib/messages/OwnershipId.py | 27 + .../trezorlib/messages/OwnershipProof.py | 30 + .../trezorlib/messages/PassphraseAck.py | 16 +- .../trezorlib/messages/PassphraseRequest.py | 12 +- .../messages/PassphraseSourceType.py | 5 - .../trezorlib/messages/PinMatrixAck.py | 14 +- .../trezorlib/messages/PinMatrixRequest.py | 15 +- .../messages/PinMatrixRequestType.py | 14 +- hwilib/devices/trezorlib/messages/Ping.py | 20 +- .../messages/PreauthorizedRequest.py | 14 + .../devices/trezorlib/messages/PublicKey.py | 17 +- .../trezorlib/messages/RecoveryDevice.py | 31 +- .../trezorlib/messages/RecoveryDeviceType.py | 10 +- .../devices/trezorlib/messages/RequestType.py | 18 +- .../devices/trezorlib/messages/ResetDevice.py | 48 +- .../trezorlib/messages/SafetyCheckLevel.py | 11 + hwilib/devices/trezorlib/messages/SelfTest.py | 12 +- .../trezorlib/messages/SignIdentity.py | 30 - .../devices/trezorlib/messages/SignMessage.py | 21 +- hwilib/devices/trezorlib/messages/SignTx.py | 43 +- .../trezorlib/messages/SignedIdentity.py | 25 - hwilib/devices/trezorlib/messages/Success.py | 12 +- .../trezorlib/messages/TransactionType.py | 39 +- hwilib/devices/trezorlib/messages/TxAck.py | 12 +- .../devices/trezorlib/messages/TxInputType.py | 57 +- .../trezorlib/messages/TxOutputBinType.py | 20 +- .../trezorlib/messages/TxOutputType.py | 42 +- .../devices/trezorlib/messages/TxRequest.py | 19 +- .../messages/TxRequestDetailsType.py | 18 +- .../messages/TxRequestSerializedType.py | 16 +- .../trezorlib/messages/VerifyMessage.py | 26 +- .../devices/trezorlib/messages/WipeDevice.py | 7 + hwilib/devices/trezorlib/messages/WordAck.py | 14 +- .../devices/trezorlib/messages/WordRequest.py | 15 +- .../trezorlib/messages/WordRequestType.py | 12 +- hwilib/devices/trezorlib/messages/__init__.py | 31 +- hwilib/devices/trezorlib/protobuf.py | 437 +++++++++---- hwilib/devices/trezorlib/tools.py | 72 ++- .../devices/trezorlib/transport/__init__.py | 36 +- hwilib/devices/trezorlib/transport/hid.py | 43 +- .../devices/trezorlib/transport/protocol.py | 71 +-- hwilib/devices/trezorlib/transport/udp.py | 38 +- hwilib/devices/trezorlib/transport/webusb.py | 29 +- hwilib/devices/trezorlib/ui.py | 111 ---- 117 files changed, 3319 insertions(+), 1492 deletions(-) create mode 100644 hwilib/devices/trezorlib/messages/AmountUnit.py create mode 100644 hwilib/devices/trezorlib/messages/AuthorizeCoinJoin.py create mode 100644 hwilib/devices/trezorlib/messages/BackupType.py create mode 100644 hwilib/devices/trezorlib/messages/Capability.py delete mode 100644 hwilib/devices/trezorlib/messages/ClearSession.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkEraseSdCard.py delete mode 100644 hwilib/devices/trezorlib/messages/DebugLinkLog.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkRecordScreen.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkReseedRandom.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkShowText.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkShowTextItem.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkShowTextStyle.py create mode 100644 hwilib/devices/trezorlib/messages/DebugLinkWatchLayout.py create mode 100644 hwilib/devices/trezorlib/messages/DebugSwipeDirection.py create mode 100644 hwilib/devices/trezorlib/messages/DoPreauthorized.py create mode 100644 hwilib/devices/trezorlib/messages/EndSession.py create mode 100644 hwilib/devices/trezorlib/messages/GetOwnershipId.py create mode 100644 hwilib/devices/trezorlib/messages/GetOwnershipProof.py delete mode 100644 hwilib/devices/trezorlib/messages/IdentityType.py create mode 100644 hwilib/devices/trezorlib/messages/LockDevice.py create mode 100644 hwilib/devices/trezorlib/messages/OwnershipId.py create mode 100644 hwilib/devices/trezorlib/messages/OwnershipProof.py delete mode 100644 hwilib/devices/trezorlib/messages/PassphraseSourceType.py create mode 100644 hwilib/devices/trezorlib/messages/PreauthorizedRequest.py create mode 100644 hwilib/devices/trezorlib/messages/SafetyCheckLevel.py delete mode 100644 hwilib/devices/trezorlib/messages/SignIdentity.py delete mode 100644 hwilib/devices/trezorlib/messages/SignedIdentity.py delete mode 100644 hwilib/devices/trezorlib/ui.py diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index ab69ef6fc..899e16d6b 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -8,7 +8,6 @@ ) from .trezorlib.transport import ( enumerate_devices, - KEEPKEY_VENDOR_IDS, ) from .trezor import TrezorClient @@ -22,10 +21,6 @@ def __init__(self, path, password='', expert=False): def enumerate(password=''): results = [] for dev in enumerate_devices(): - # enumerate_devices filters to Trezors and Keepkeys. - # Only allow Keepkeys and unknowns. Unknown devices will reach the check for vendor later - if dev.get_usb_vendor_id() not in KEEPKEY_VENDOR_IDS | {-1}: - continue d_data = {} d_data['type'] = 'keepkey' diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index f4cd12703..e3fb98469 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -25,7 +25,6 @@ from .trezorlib.transport import ( enumerate_devices, get_transport, - TREZOR_VENDOR_IDS, ) from .trezorlib.ui import ( echo, @@ -588,10 +587,6 @@ def toggle_passphrase(self): def enumerate(password=''): results = [] for dev in enumerate_devices(): - # enumerate_devices filters to Trezors and Keepkeys. - # Only allow Trezors and unknowns. Unknown devices will reach the check for vendor later - if dev.get_usb_vendor_id() not in TREZOR_VENDOR_IDS | {-1}: - continue d_data = {} d_data['type'] = 'trezor' diff --git a/hwilib/devices/trezorlib/README.md b/hwilib/devices/trezorlib/README.md index 8b5d295b0..dd09fbffd 100644 --- a/hwilib/devices/trezorlib/README.md +++ b/hwilib/devices/trezorlib/README.md @@ -1,13 +1,12 @@ # Python Trezor Library -This is a stripped down version of the official [python-trezor](https://github.com/trezor/python-trezor) library. +This is a stripped down version of the official [python-trezor](https://github.com/trezor/trezor-firmware/tree/master/python) library. -This stripped down version was made at commit [d5c2636f0d1b7da3cb94a4eff6169d77f58cefc1](https://github.com/trezor/python-trezor/tree/d5c2636f0d1b7da3cb94a4eff6169d77f58cefc1). +This stripped down version was made at commit [e4c406822c00695aaf7cd420634643236de17849](https://github.com/trezor/trezor-firmware/commit/e4c406822c00695aaf7cd420634643236de17849). ## Changes - Removed altcoin support -- Include the compiled protobuf definitions instead of making them on install - Removed functions that HWI does not use or plan to use - Changed `TrezorClient` from calling `init_device()` (HWI needs this behavior and doing it in the library makes this simpler) - Add Keepkey support. Some fields of some messages had to be removed to support both the Keepkey and the Trezor in the same library diff --git a/hwilib/devices/trezorlib/__init__.py b/hwilib/devices/trezorlib/__init__.py index 029f0032e..6db406059 100644 --- a/hwilib/devices/trezorlib/__init__.py +++ b/hwilib/devices/trezorlib/__init__.py @@ -1,9 +1,8 @@ -__version__ = "0.11.1" +__version__ = "0.13.0" # fmt: off MINIMUM_FIRMWARE_VERSION = { - "1": (1, 6, 1), - "T": (2, 0, 10), - "K1-14AM": (0, 0, 0) + "1": (1, 8, 0), + "T": (2, 1, 0), } # fmt: on diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index 7e97758ea..775dcf767 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,12 +14,49 @@ # You should have received a copy of the License along with this library. # If not, see . -import binascii -from typing import Union +import warnings +from copy import copy +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Dict, Sequence, Tuple -from . import messages +from . import exceptions, messages from .tools import expect, normalize_nfc, session +if TYPE_CHECKING: + from .client import TrezorClient + + +def from_json(json_dict): + def make_input(vin): + if "coinbase" in vin: + return messages.TxInputType( + prev_hash=b"\0" * 32, + prev_index=0xFFFFFFFF, # signed int -1 + script_sig=bytes.fromhex(vin["coinbase"]), + sequence=vin["sequence"], + ) + + else: + return messages.TxInputType( + prev_hash=bytes.fromhex(vin["txid"]), + prev_index=vin["vout"], + script_sig=bytes.fromhex(vin["scriptSig"]["hex"]), + sequence=vin["sequence"], + ) + + def make_bin_output(vout): + return messages.TxOutputBinType( + amount=int(Decimal(vout["value"]) * (10 ** 8)), + script_pubkey=bytes.fromhex(vout["scriptPubKey"]["hex"]), + ) + + return messages.TransactionType( + version=json_dict["version"], + lock_time=json_dict.get("locktime", 0), + inputs=[make_input(vin) for vin in json_dict["vin"]], + bin_outputs=[make_bin_output(vout) for vout in json_dict["vout"]], + ) + @expect(messages.PublicKey) def get_public_node( @@ -29,6 +66,7 @@ def get_public_node( show_display=False, coin_name=None, script_type=messages.InputScriptType.SPENDADDRESS, + ignore_xpub_magic=False, ): return client.call( messages.GetPublicKey( @@ -37,6 +75,7 @@ def get_public_node( show_display=show_display, coin_name=coin_name, script_type=script_type, + ignore_xpub_magic=ignore_xpub_magic, ) ) @@ -49,6 +88,7 @@ def get_address( show_display=False, multisig=None, script_type=messages.InputScriptType.SPENDADDRESS, + ignore_xpub_magic=False, ): return client.call( messages.GetAddress( @@ -57,17 +97,66 @@ def get_address( show_display=show_display, multisig=multisig, script_type=script_type, + ignore_xpub_magic=ignore_xpub_magic, ) ) -@expect(messages.MessageSignature) -def sign_message( +@expect(messages.OwnershipId, field="ownership_id") +def get_ownership_id( + client, + coin_name, + n, + multisig=None, + script_type=messages.InputScriptType.SPENDADDRESS, +): + return client.call( + messages.GetOwnershipId( + address_n=n, + coin_name=coin_name, + multisig=multisig, + script_type=script_type, + ) + ) + + +def get_ownership_proof( client, coin_name, n, - message: Union[str, bytes], + multisig=None, script_type=messages.InputScriptType.SPENDADDRESS, + user_confirmation=False, + ownership_ids=None, + commitment_data=None, + preauthorized=False, +): + if preauthorized: + res = client.call(messages.DoPreauthorized()) + if not isinstance(res, messages.PreauthorizedRequest): + raise exceptions.TrezorException("Unexpected message") + + res = client.call( + messages.GetOwnershipProof( + address_n=n, + coin_name=coin_name, + script_type=script_type, + multisig=multisig, + user_confirmation=user_confirmation, + ownership_ids=ownership_ids, + commitment_data=commitment_data, + ) + ) + + if not isinstance(res, messages.OwnershipProof): + raise exceptions.TrezorException("Unexpected message") + + return res.ownership_proof, res.signature + + +@expect(messages.MessageSignature) +def sign_message( + client, coin_name, n, message, script_type=messages.InputScriptType.SPENDADDRESS ): message = normalize_nfc(message) return client.call( @@ -77,18 +166,72 @@ def sign_message( ) +def verify_message(client, coin_name, address, signature, message): + message = normalize_nfc(message) + try: + resp = client.call( + messages.VerifyMessage( + address=address, + signature=signature, + message=message, + coin_name=coin_name, + ) + ) + except exceptions.TrezorFailure: + return False + return isinstance(resp, messages.Success) + + @session -def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None): - this_tx = messages.TransactionType(inputs=inputs, outputs=outputs) +def sign_tx( + client: "TrezorClient", + coin_name: str, + inputs: Sequence[messages.TxInputType], + outputs: Sequence[messages.TxOutputType], + details: messages.SignTx = None, + prev_txes: Dict[bytes, messages.TransactionType] = None, + preauthorized: bool = False, + **kwargs: Any, +) -> Tuple[Sequence[bytes], bytes]: + """Sign a Bitcoin-like transaction. - if details is None: - signtx = messages.SignTx() - else: + Returns a list of signatures (one for each provided input) and the + network-serialized transaction. + + In addition to the required arguments, it is possible to specify additional + transaction properties (version, lock time, expiry...). Each additional argument + must correspond to a field in the `SignTx` data type. Note that some fields + (`inputs_count`, `outputs_count`, `coin_name`) will be inferred from the arguments + and cannot be overriden by kwargs. + """ + if prev_txes is None: + prev_txes = {} + + if details is not None: + warnings.warn( + "'details' argument is deprecated, use kwargs instead", + DeprecationWarning, + stacklevel=2, + ) signtx = details + signtx.coin_name = coin_name + signtx.inputs_count = len(inputs) + signtx.outputs_count = len(outputs) + + else: + signtx = messages.SignTx( + coin_name=coin_name, + inputs_count=len(inputs), + outputs_count=len(outputs), + ) + for name, value in kwargs.items(): + if hasattr(signtx, name): + setattr(signtx, name, value) - signtx.coin_name = coin_name - signtx.inputs_count = len(inputs) - signtx.outputs_count = len(outputs) + if preauthorized: + res = client.call(messages.DoPreauthorized()) + if not isinstance(res, messages.PreauthorizedRequest): + raise exceptions.TrezorException("Unexpected message") res = client.call(signtx) @@ -96,8 +239,8 @@ def sign_tx(client, coin_name, inputs, outputs, details=None, prev_txes=None): signatures = [None] * len(inputs) serialized_tx = b"" - def copy_tx_meta(tx): - tx_copy = messages.TransactionType(**tx) + def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: + tx_copy = copy(tx) # clear fields tx_copy.inputs_cnt = len(tx.inputs) tx_copy.inputs = [] @@ -108,6 +251,15 @@ def copy_tx_meta(tx): tx_copy.extra_data = None return tx_copy + this_tx = messages.TransactionType( + inputs=inputs, + outputs=outputs, + inputs_cnt=len(inputs), + outputs_cnt=len(outputs), + # pick either kw-provided or default value from the SignTx request + version=signtx.version, + ) + R = messages.RequestType while isinstance(res, messages.TxRequest): # If there's some part of signed transaction, let's add it @@ -127,8 +279,6 @@ def copy_tx_meta(tx): # Device asked for one more information, let's process it. if res.details.tx_hash is not None: - if res.details.tx_hash not in prev_txes: - raise ValueError('Previous transaction {} not available'.format(binascii.hexlify(res.details.tx_hash))) current_tx = prev_txes[res.details.tx_hash] else: current_tx = this_tx @@ -137,7 +287,7 @@ def copy_tx_meta(tx): msg = copy_tx_meta(current_tx) res = client.call(messages.TxAck(tx=msg)) - elif res.request_type == R.TXINPUT: + elif res.request_type in (R.TXINPUT, R.TXORIGINPUT): msg = messages.TransactionType() msg.inputs = [current_tx.inputs[res.details.request_index]] res = client.call(messages.TxAck(tx=msg)) @@ -151,6 +301,11 @@ def copy_tx_meta(tx): res = client.call(messages.TxAck(tx=msg)) + elif res.request_type == R.TXORIGOUTPUT: + msg = messages.TransactionType() + msg.outputs = [current_tx.outputs[res.details.request_index]] + res = client.call(messages.TxAck(tx=msg)) + elif res.request_type == R.TXEXTRADATA: o, l = res.details.extra_data_offset, res.details.extra_data_len msg = messages.TransactionType() @@ -160,7 +315,30 @@ def copy_tx_meta(tx): if not isinstance(res, messages.TxRequest): raise exceptions.TrezorException("Unexpected message") - if None in signatures: - raise exceptions.TrezorException("Some signatures are missing!") + for i, sig in zip(inputs, signatures): + if i.script_type != messages.InputScriptType.EXTERNAL and sig is None: + raise exceptions.TrezorException("Some signatures are missing!") return signatures, serialized_tx + + +@expect(messages.Success, field="message") +def authorize_coinjoin( + client, + coordinator, + max_total_fee, + n, + coin_name, + fee_per_anonymity=None, + script_type=messages.InputScriptType.SPENDADDRESS, +): + return client.call( + messages.AuthorizeCoinJoin( + coordinator=coordinator, + max_total_fee=max_total_fee, + address_n=n, + coin_name=coin_name, + fee_per_anonymity=fee_per_anonymity, + script_type=script_type, + ) + ) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index a802c32f2..a75be139a 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -15,26 +15,23 @@ # If not, see . import logging -import sys +import os import warnings +from typing import Optional from mnemonic import Mnemonic -from . import MINIMUM_FIRMWARE_VERSION, exceptions, messages, tools - -if sys.version_info.major < 3: - raise Exception("Trezorlib does not support Python 2 anymore.") +from . import MINIMUM_FIRMWARE_VERSION, exceptions, mapping, messages, tools +from .log import DUMP_BYTES +from .messages import Capability LOG = logging.getLogger(__name__) -VENDORS = ("bitcointrezor.com", "trezor.io", "keepkey.com") +VENDORS = ("bitcointrezor.com", "trezor.io") MAX_PASSPHRASE_LENGTH = 50 -DEPRECATION_ERROR = """ -Incompatible Trezor library detected. - -(Original error: {}) -""".strip() +PASSPHRASE_ON_DEVICE = object() +PASSPHRASE_TEST_PATH = tools.parse_path("44h/1h/0h/0/0") OUTDATED_FIRMWARE_ERROR = """ Your Trezor firmware is out of date. Update it with the following command: @@ -43,13 +40,27 @@ """.strip() -def get_buttonrequest_value(code): - # Converts integer code to its string representation of ButtonRequestType - return [ - k - for k in dir(messages.ButtonRequestType) - if getattr(messages.ButtonRequestType, k) == code - ][0] +def get_default_client(path=None, ui=None, **kwargs): + """Get a client for a connected Trezor device. + + Returns a TrezorClient instance with minimum fuss. + + If path is specified, does a prefix-search for the specified device. Otherwise, uses + the value of TREZOR_PATH env variable, or finds first connected Trezor. + If no UI is supplied, instantiates the default CLI UI. + """ + from .transport import get_transport + from .ui import ClickUI + + if path is None: + path = os.getenv("TREZOR_PATH") + + transport = get_transport(path, prefix_search=True) + if ui is None: + ui = ClickUI() + + return TrezorClient(transport, ui, **kwargs) + class TrezorClient: """Trezor client, a connection to a Trezor device. @@ -67,20 +78,22 @@ class TrezorClient: - passphrase request (ask the user to enter a passphrase) See `trezorlib.ui` for details. - You can supply a `state` you saved in the previous session. If you do, - the user might not need to enter their passphrase again. + You can supply a `session_id` you might have saved in the previous session. + If you do, the user might not need to enter their passphrase again. """ - def __init__(self, transport, ui=None, state=None): - LOG.debug("creating client instance for device: {}".format(transport.get_path())) + def __init__( + self, + transport, + ui, + session_id=None, + ): + LOG.info("creating client instance for device: {}".format(transport.get_path())) self.transport = transport self.ui = ui - self.state = state - - if ui is None: - warnings.warn("UI class not supplied. This will probably crash soon.") - self.session_counter = 0 + self.session_id = session_id + self.init_device(session_id=session_id) def open(self): if self.session_counter == 0: @@ -88,9 +101,10 @@ def open(self): self.session_counter += 1 def close(self): - if self.session_counter == 1: + self.session_counter = max(self.session_counter - 1, 0) + if self.session_counter == 0: + # TODO call EndSession here? self.transport.end_session() - self.session_counter -= 1 def cancel(self): self._raw_write(messages.Cancel()) @@ -102,11 +116,34 @@ def call_raw(self, msg): def _raw_write(self, msg): __tracebackhide__ = True # for pytest # pylint: disable=W0612 - self.transport.write(msg) + LOG.debug( + "sending message: {}".format(msg.__class__.__name__), + extra={"protobuf": msg}, + ) + msg_type, msg_bytes = mapping.encode(msg) + LOG.log( + DUMP_BYTES, + "encoded as type {} ({} bytes): {}".format( + msg_type, len(msg_bytes), msg_bytes.hex() + ), + ) + self.transport.write(msg_type, msg_bytes) def _raw_read(self): __tracebackhide__ = True # for pytest # pylint: disable=W0612 - return self.transport.read() + msg_type, msg_bytes = self.transport.read() + LOG.log( + DUMP_BYTES, + "received type {} ({} bytes): {}".format( + msg_type, len(msg_bytes), msg_bytes.hex() + ), + ) + msg = mapping.decode(msg_type, msg_bytes) + LOG.debug( + "received message: {}".format(msg.__class__.__name__), + extra={"protobuf": msg}, + ) + return msg def _callback_pin(self, msg): try: @@ -115,9 +152,9 @@ def _callback_pin(self, msg): self.call_raw(messages.Cancel()) raise - if not pin.isdigit(): + if any(d not in "123456789" for d in pin) or not (1 <= len(pin) <= 9): self.call_raw(messages.Cancel()) - raise ValueError("Non-numeric PIN provided") + raise ValueError("Invalid PIN provided") resp = self.call_raw(messages.PinMatrixAck(pin=pin)) if isinstance(resp, messages.Failure) and resp.code in ( @@ -129,14 +166,14 @@ def _callback_pin(self, msg): else: return resp - def _callback_passphrase(self, msg): - available_on_device = self.features.model == 'T' + def _callback_passphrase(self, msg: messages.PassphraseRequest): + available_on_device = Capability.PassphraseEntry in self.features.capabilities def send_passphrase(passphrase=None, on_device=None): msg = messages.PassphraseAck(passphrase=passphrase, on_device=on_device) resp = self.call_raw(msg) if isinstance(resp, messages.Deprecated_PassphraseStateRequest): - self.session_id = resp._state + self.session_id = resp.state resp = self.call_raw(messages.Deprecated_PassphraseStateAck()) return resp @@ -144,22 +181,26 @@ def send_passphrase(passphrase=None, on_device=None): if msg._on_device is True: return send_passphrase(None, None) - if available_on_device: - return send_passphrase(on_device=True) - try: - passphrase = self.ui.get_passphrase() - except Exception: + passphrase = self.ui.get_passphrase(available_on_device=available_on_device) + except exceptions.Cancelled: self.call_raw(messages.Cancel()) raise + if passphrase is PASSPHRASE_ON_DEVICE: + if not available_on_device: + self.call_raw(messages.Cancel()) + raise RuntimeError("Device is not capable of entering passphrase") + else: + return send_passphrase(on_device=True) + # else process host-entered passphrase passphrase = Mnemonic.normalize_string(passphrase) if len(passphrase) > MAX_PASSPHRASE_LENGTH: self.call_raw(messages.Cancel()) raise ValueError("Passphrase too long") - return send_passphrase(passphrase=passphrase) + return send_passphrase(passphrase, on_device=False) def _callback_button(self, msg): __tracebackhide__ = True # for pytest # pylint: disable=W0612 @@ -186,42 +227,91 @@ def call(self, msg): else: return resp - @tools.session - def init_device(self): - resp = self.call_raw(messages.GetFeatures()) - # If GetFeatures fails, try initializing and clearing inconsistent state on the device - if isinstance(resp, messages.Failure): - resp = self.call_raw(messages.Initialize()) - if not isinstance(resp, messages.Features): - raise exceptions.TrezorException("Unexpected initial response") - else: - # If this is a Trezor One or Keepkey, do Initialize - if resp.model == '1' or resp.model == 'K1-14AM': - resp = self.call_raw(messages.Initialize()) - if not isinstance(resp, messages.Features): - raise exceptions.TrezorException("Unexpected initial response") - # For the T, we need to check if a passphrase needs to be entered - elif resp.model == 'T': - # Try GetPublicKey. If it fails, we try to send Initialize - pubkey_resp = self.call_raw(messages.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000])) - if isinstance(pubkey_resp, messages.Failure): - resp = self.call_raw(messages.Initialize()) - if not isinstance(resp, messages.Features): - raise exceptions.TrezorException("Unexpected initial response") - elif isinstance(pubkey_resp, messages.PassphraseRequest): - self.call_raw(messages.Cancel()) - self.features = resp - if self.features.vendor not in VENDORS: + def _refresh_features(self, features: messages.Features) -> None: + """Update internal fields based on passed-in Features message.""" + if features.vendor not in VENDORS: raise RuntimeError("Unsupported device") - # A side-effect of this is a sanity check for broken protobuf definitions. - # If the `vendor` field doesn't exist, you probably have a mismatched - # checkout of trezor-common. + + self.features = features self.version = ( self.features.major_version, self.features.minor_version, self.features.patch_version, ) self.check_firmware_version(warn_only=True) + if self.features.session_id is not None: + self.session_id = self.features.session_id + self.features.session_id = None + + @tools.session + def refresh_features(self) -> messages.Features: + """Reload features from the device. + + Should be called after changing settings or performing operations that affect + device state. + """ + resp = self.call_raw(messages.GetFeatures()) + if not isinstance(resp, messages.Features): + raise exceptions.TrezorException("Unexpected response to GetFeatures") + self._refresh_features(resp) + return resp + + @tools.session + def init_device( + self, *, session_id: bytes = None, new_session: bool = False + ) -> Optional[bytes]: + """Initialize the device and return a session ID. + + You can optionally specify a session ID. If the session still exists on the + device, the same session ID will be returned and the session is resumed. + Otherwise a different session ID is returned. + + Specify `new_session=True` to open a fresh session. Since firmware version + 1.9.0/2.3.0, the previous session will remain cached on the device, and can be + resumed by calling `init_device` again with the appropriate session ID. + + If neither `new_session` nor `session_id` is specified, the current session ID + will be reused. If no session ID was cached, a new session ID will be allocated + and returned. + + # Version notes: + + Trezor One older than 1.9.0 does not have session management. Optional arguments + have no effect and the function returns None + + Trezor T older than 2.3.0 does not have session cache. Requesting a new session + will overwrite the old one. In addition, this function will always return None. + A valid session_id can be obtained from the `session_id` attribute, but only + after a passphrase-protected call is performed. You can use the following code: + + >>> client.init_device() + >>> client.ensure_unlocked() + >>> valid_session_id = client.session_id + """ + if new_session: + self.session_id = None + elif session_id is not None: + self.session_id = session_id + + resp = self.call_raw(messages.Initialize(session_id=self.session_id)) + if not isinstance(resp, messages.Features): + raise exceptions.TrezorException("Unexpected response to Initialize") + + if self.session_id is not None and resp.session_id == self.session_id: + LOG.info("Successfully resumed session") + elif session_id is not None: + LOG.info("Failed to resume session") + + # TT < 2.3.0 compatibility: + # _refresh_features will clear out the session_id field. We want this function + # to return its value, so that callers can rely on it being either a valid + # session_id, or None if we can't do that. + # Older TT FW does not report session_id in Features and self.session_id might + # be invalid because TT will not allocate a session_id until a passphrase + # exchange happens. + reported_session_id = resp.session_id + self._refresh_features(resp) + return reported_session_id def is_outdated(self): if self.features.bootloader_mode: @@ -233,7 +323,7 @@ def is_outdated(self): def check_firmware_version(self, warn_only=False): if self.is_outdated(): if warn_only: - warnings.warn(OUTDATED_FIRMWARE_ERROR, stacklevel=2) + warnings.warn("Firmware is out of date", stacklevel=2) else: raise exceptions.OutdatedFirmwareError(OUTDATED_FIRMWARE_ERROR) @@ -242,33 +332,95 @@ def ping( self, msg, button_protection=False, - pin_protection=False, - passphrase_protection=False, ): # We would like ping to work on any valid TrezorClient instance, but # due to the protection modes, we need to go through self.call, and that will # raise an exception if the firmware is too old. # So we short-circuit the simplest variant of ping with call_raw. - if not button_protection and not pin_protection and not passphrase_protection: + if not button_protection: # XXX this should be: `with self:` try: self.open() - return self.call_raw(messages.Ping(message=msg)) + resp = self.call_raw(messages.Ping(message=msg)) + if isinstance(resp, messages.ButtonRequest): + # device is PIN-locked. + # respond and hope for the best + resp = self._callback_button(resp) + return resp finally: self.close() - msg = messages.Ping( - message=msg, - button_protection=button_protection, - pin_protection=pin_protection, - passphrase_protection=passphrase_protection, - ) + msg = messages.Ping(message=msg, button_protection=button_protection) return self.call(msg) def get_device_id(self): return self.features.device_id - @tools.expect(messages.Success, field="message") + @tools.session + def lock(self, *, _refresh_features=True): + """Lock the device. + + If the device does not have a PIN configured, this will do nothing. + Otherwise, a lock screen will be shown and the device will prompt for PIN + before further actions. + + This call does _not_ invalidate passphrase cache. If passphrase is in use, + the device will not prompt for it after unlocking. + + To invalidate passphrase cache, use `end_session()`. To lock _and_ invalidate + passphrase cache, use `clear_session()`. + """ + # Private argument _refresh_features can be used internally to avoid + # refreshing in cases where we will refresh soon anyway. This is used + # in TrezorClient.clear_session() + self.call(messages.LockDevice()) + if _refresh_features: + self.refresh_features() + + @tools.session + def ensure_unlocked(self): + """Ensure the device is unlocked and a passphrase is cached. + + If the device is locked, this will prompt for PIN. If passphrase is enabled + and no passphrase is cached for the current session, the device will also + prompt for passphrase. + + After calling this method, further actions on the device will not prompt for + PIN or passphrase until the device is locked or the session becomes invalid. + """ + from .btc import get_address + + get_address(self, "Testnet", PASSPHRASE_TEST_PATH) + self.refresh_features() + + def end_session(self): + """Close the current session and clear cached passphrase. + + The session will become invalid until `init_device()` is called again. + If passphrase is enabled, further actions will prompt for it again. + + This is a no-op in bootloader mode, as it does not support session management. + """ + # since: 2.3.4, 1.9.4 + try: + if not self.features.bootloader_mode: + self.call(messages.EndSession()) + except exceptions.TrezorFailure: + # A failure most likely means that the FW version does not support + # the EndSession call. We ignore the failure and clear the local session_id. + # The client-side end result is identical. + pass + self.session_id = None + @tools.session def clear_session(self): - return self.call_raw(messages.ClearSession()) + """Lock the device and present a fresh session. + + The current session will be invalidated and a new one will be started. If the + device has PIN enabled, it will become locked. + + Equivalent to calling `lock()`, `end_session()` and `init_device()`. + """ + self.lock(_refresh_features=False) + self.end_session() + self.init_device(new_session=True) diff --git a/hwilib/devices/trezorlib/debuglink.py b/hwilib/devices/trezorlib/debuglink.py index b7c8a40b9..c64ba909a 100644 --- a/hwilib/devices/trezorlib/debuglink.py +++ b/hwilib/devices/trezorlib/debuglink.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,16 +14,29 @@ # You should have received a copy of the License along with this library. # If not, see . +import logging +import textwrap +from collections import namedtuple from copy import deepcopy from mnemonic import Mnemonic -from . import messages as proto, protobuf, tools +from . import mapping, messages, protobuf from .client import TrezorClient +from .exceptions import TrezorFailure +from .log import DUMP_BYTES from .tools import expect EXPECTED_RESPONSES_CONTEXT_LINES = 3 +LayoutLines = namedtuple("LayoutLines", "lines text") + +LOG = logging.getLogger(__name__) + + +def layout_lines(lines): + return LayoutLines(lines, " ".join(lines)) + class DebugLink: def __init__(self, transport, auto_interact=True): @@ -37,72 +50,96 @@ def close(self): self.transport.end_session() def _call(self, msg, nowait=False): - self.transport.write(msg) + LOG.debug( + "sending message: {}".format(msg.__class__.__name__), + extra={"protobuf": msg}, + ) + msg_type, msg_bytes = mapping.encode(msg) + LOG.log( + DUMP_BYTES, + "encoded as type {} ({} bytes): {}".format( + msg_type, len(msg_bytes), msg_bytes.hex() + ), + ) + self.transport.write(msg_type, msg_bytes) if nowait: return None - ret = self.transport.read() - return ret + + ret_type, ret_bytes = self.transport.read() + LOG.log( + DUMP_BYTES, + "received type {} ({} bytes): {}".format( + msg_type, len(msg_bytes), msg_bytes.hex() + ), + ) + msg = mapping.decode(ret_type, ret_bytes) + LOG.debug( + "received message: {}".format(msg.__class__.__name__), + extra={"protobuf": msg}, + ) + return msg def state(self): - return self._call(proto.DebugLinkGetState()) + return self._call(messages.DebugLinkGetState()) - def read_pin(self): - state = self.state() - return state.pin, state.matrix + def read_layout(self): + return layout_lines(self.state().layout_lines) + + def wait_layout(self): + obj = self._call(messages.DebugLinkGetState(wait_layout=True)) + if isinstance(obj, messages.Failure): + raise TrezorFailure(obj) + return layout_lines(obj.layout_lines) + + def watch_layout(self, watch: bool) -> None: + """Enable or disable watching layouts. + If disabled, wait_layout will not work. - def read_pin_encoded(self): - return self.encode_pin(*self.read_pin()) + The message is missing on T1. Use `TrezorClientDebugLink.watch_layout` for + cross-version compatibility. + """ + self._call(messages.DebugLinkWatchLayout(watch=watch)) def encode_pin(self, pin, matrix=None): """Transform correct PIN according to the displayed matrix.""" if matrix is None: - _, matrix = self.read_pin() - return "".join([str(matrix.index(p) + 1) for p in pin]) - - def read_layout(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.layout + matrix = self.state().matrix + if matrix is None: + # we are on trezor-core + return pin - def read_mnemonic(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.mnemonic + return "".join([str(matrix.index(p) + 1) for p in pin]) def read_recovery_word(self): - obj = self._call(proto.DebugLinkGetState()) - return (obj.recovery_fake_word, obj.recovery_word_pos) + state = self.state() + return (state.recovery_fake_word, state.recovery_word_pos) def read_reset_word(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.reset_word + state = self._call(messages.DebugLinkGetState(wait_word_list=True)) + return state.reset_word def read_reset_word_pos(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.reset_word_pos - - def read_reset_entropy(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.reset_entropy - - def read_passphrase_protection(self): - obj = self._call(proto.DebugLinkGetState()) - return obj.passphrase_protection + state = self._call(messages.DebugLinkGetState(wait_word_pos=True)) + return state.reset_word_pos - def input(self, word=None, button=None, swipe=None): + def input(self, word=None, button=None, swipe=None, x=None, y=None, wait=False): if not self.allow_interactions: return - decision = proto.DebugLinkDecision() - if button is not None: - decision.yes_no = button - elif word is not None: - decision.input = word - elif swipe is not None: - decision.up_down = swipe - else: - raise ValueError("You need to provide input data.") - self._call(decision, nowait=True) - def press_button(self, yes_no): - self._call(proto.DebugLinkDecision(yes_no=yes_no), nowait=True) + args = sum(a is not None for a in (word, button, swipe, x)) + if args != 1: + raise ValueError("Invalid input - must use one of word, button, swipe") + + decision = messages.DebugLinkDecision( + yes_no=button, swipe=swipe, input=word, x=x, y=y, wait=wait + ) + ret = self._call(decision, nowait=not wait) + if ret is not None: + return layout_lines(ret.lines) + + def click(self, click, wait=False): + x, y = click + return self.input(x=x, y=y, wait=wait) def press_yes(self): self.input(button=True) @@ -111,26 +148,45 @@ def press_no(self): self.input(button=False) def swipe_up(self): - self.input(swipe=True) + self.input(swipe=messages.DebugSwipeDirection.UP) def swipe_down(self): - self.input(swipe=False) + self.input(swipe=messages.DebugSwipeDirection.DOWN) + + def swipe_right(self): + self.input(swipe=messages.DebugSwipeDirection.RIGHT) + + def swipe_left(self): + self.input(swipe=messages.DebugSwipeDirection.LEFT) def stop(self): - self._call(proto.DebugLinkStop(), nowait=True) + self._call(messages.DebugLinkStop(), nowait=True) - @expect(proto.DebugLinkMemory, field="memory") + def reseed(self, value): + return self._call(messages.DebugLinkReseedRandom(value=value)) + + def start_recording(self, directory): + self._call(messages.DebugLinkRecordScreen(target_directory=directory)) + + def stop_recording(self): + self._call(messages.DebugLinkRecordScreen(target_directory=None)) + + @expect(messages.DebugLinkMemory, field="memory") def memory_read(self, address, length): - return self._call(proto.DebugLinkMemoryRead(address=address, length=length)) + return self._call(messages.DebugLinkMemoryRead(address=address, length=length)) def memory_write(self, address, memory, flash=False): self._call( - proto.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash), + messages.DebugLinkMemoryWrite(address=address, memory=memory, flash=flash), nowait=True, ) def flash_erase(self, sector): - self._call(proto.DebugLinkFlashErase(sector=sector), nowait=True) + self._call(messages.DebugLinkFlashErase(sector=sector), nowait=True) + + @expect(messages.Success) + def erase_sd_card(self, format=True): + return self._call(messages.DebugLinkEraseSdCard(format=format)) class NullDebugLink(DebugLink): @@ -145,8 +201,8 @@ def close(self): def _call(self, msg, nowait=False): if not nowait: - if isinstance(msg, proto.DebugLinkGetState): - return proto.DebugLinkState() + if isinstance(msg, messages.DebugLinkGetState): + return messages.DebugLinkState() else: raise RuntimeError("unexpected call to a fake debuglink") @@ -156,14 +212,19 @@ class DebugUI: def __init__(self, debuglink: DebugLink): self.debuglink = debuglink - self.pin = None - self.passphrase = "sphinx of black quartz, judge my wov" + self.clear() + + def clear(self): + self.pins = None + self.passphrase = "" self.input_flow = None - self.return_passphrase = True def button_request(self, code): if self.input_flow is None: - self.debuglink.press_yes() + if code == messages.ButtonRequestType.PinEntry: + self.debuglink.input(self.get_pin()) + else: + self.debuglink.press_yes() elif self.input_flow is self.INPUT_FLOW_DONE: raise AssertionError("input flow ended prematurely") else: @@ -173,18 +234,106 @@ def button_request(self, code): self.input_flow = self.INPUT_FLOW_DONE def get_pin(self, code=None): - if self.pin: - return self.pin + if self.pins is None: + raise RuntimeError("PIN requested but no sequence was configured") + + try: + return self.debuglink.encode_pin(next(self.pins)) + except StopIteration: + raise AssertionError("PIN sequence ended prematurely") + + def get_passphrase(self, available_on_device): + return self.passphrase + + +class MessageFilter: + def __init__(self, message_type, **fields): + self.message_type = message_type + self.fields = {} + self.update_fields(**fields) + + def update_fields(self, **fields): + for name, value in fields.items(): + try: + self.fields[name] = self.from_message_or_type(value) + except TypeError: + self.fields[name] = value + + return self + + @classmethod + def from_message_or_type(cls, message_or_type): + if isinstance(message_or_type, cls): + return message_or_type + if isinstance(message_or_type, protobuf.MessageType): + return cls.from_message(message_or_type) + if isinstance(message_or_type, type) and issubclass( + message_or_type, protobuf.MessageType + ): + return cls(message_or_type) + raise TypeError("Invalid kind of expected response") + + @classmethod + def from_message(cls, message): + fields = {} + for fname, _, _ in message.get_fields().values(): + value = getattr(message, fname) + if value in (None, [], protobuf.FLAG_REQUIRED): + continue + fields[fname] = value + return cls(type(message), **fields) + + def match(self, message): + if type(message) != self.message_type: + return False + + for field, expected_value in self.fields.items(): + actual_value = getattr(message, field, None) + if isinstance(expected_value, MessageFilter): + if not expected_value.match(actual_value): + return False + elif expected_value != actual_value: + return False + + return True + + def format(self, maxwidth=80): + fields = [] + for fname, ftype, _ in self.message_type.get_fields().values(): + if fname not in self.fields: + continue + value = self.fields[fname] + if isinstance(ftype, protobuf.EnumType) and isinstance(value, int): + field_str = ftype.to_str(value) + elif isinstance(value, MessageFilter): + field_str = value.format(maxwidth - 4) + elif isinstance(value, protobuf.MessageType): + field_str = protobuf.format_message(value) + else: + field_str = repr(value) + field_str = textwrap.indent(field_str, " ").lstrip() + fields.append((fname, field_str)) + + pairs = ["{}={}".format(k, v) for k, v in fields] + oneline_str = ", ".join(pairs) + if len(oneline_str) < maxwidth: + return "{}({})".format(self.message_type.__name__, oneline_str) else: - return self.debuglink.read_pin_encoded() + item = [] + item.append("{}(".format(self.message_type.__name__)) + for pair in pairs: + item.append(" {}".format(pair)) + item.append(")") + return "\n".join(item) + + +class MessageFilterGenerator: + def __getattr__(self, key): + message_type = getattr(messages, key) + return MessageFilter(message_type).update_fields - def disallow_passphrase(self): - self.return_passphrase = False - def get_passphrase(self): - if self.return_passphrase: - return self.passphrase - raise ValueError('Passphrase from Host is not allowed for Trezor T') +message_filters = MessageFilterGenerator() class TrezorClientDebugLink(TrezorClient): @@ -202,6 +351,9 @@ def __init__(self, transport, auto_interact=True): try: debug_transport = transport.find_debug() self.debug = DebugLink(debug_transport, auto_interact) + # try to open debuglink, see if it works + self.debug.open() + self.debug.close() except Exception: if not auto_interact: self.debug = NullDebugLink() @@ -215,26 +367,32 @@ def __init__(self, transport, auto_interact=True): self.filters = {} - # Always press Yes and provide correct pin - self.setup_debuglink(True, True) - # Do not expect any specific response from device self.expected_responses = None self.current_response = None - # Use blank passphrase - self.set_passphrase("") super().__init__(transport, ui=self.ui) def open(self): super().open() - self.debug.open() + if self.session_counter == 1: + self.debug.open() def close(self): - self.debug.close() + if self.session_counter == 1: + self.debug.close() super().close() def set_filter(self, message_type, callback): + """Configure a filter function for a specified message type. + + The `callback` must be a function that accepts a protobuf message, and returns + a (possibly modified) protobuf message of the same type. Whenever a message + is sent or received that matches `message_type`, `callback` is invoked on the + message and its result is substituted for the original. + + Useful for test scenarios with an active malicious actor on the wire. + """ self.filters[message_type] = callback def _filter_message(self, msg): @@ -246,16 +404,52 @@ def _filter_message(self, msg): return msg def set_input_flow(self, input_flow): - if input_flow is None: - self.ui.input_flow = None - return + """Configure a sequence of input events for the current with-block. + + The `input_flow` must be a generator function. A `yield` statement in the + input flow function waits for a ButtonRequest from the device, and returns + its code. + + Example usage: + + >>> def input_flow(): + >>> # wait for first button prompt + >>> code = yield + >>> assert code == ButtonRequestType.Other + >>> # press No + >>> client.debug.press_no() + >>> + >>> # wait for second button prompt + >>> yield + >>> # press Yes + >>> client.debug.press_yes() + >>> + >>> with client: + >>> client.set_input_flow(input_flow) + >>> some_call(client) + """ + if not self.in_with_statement: + raise RuntimeError("Must be called inside 'with' statement") if callable(input_flow): input_flow = input_flow() if not hasattr(input_flow, "send"): raise RuntimeError("input_flow should be a generator function") self.ui.input_flow = input_flow - next(input_flow) # can't send before first yield + input_flow.send(None) # start the generator + + def watch_layout(self, watch: bool = True) -> None: + """Enable or disable watching layout changes. + + Since trezor-core v2.3.2, it is necessary to call `watch_layout()` before + using `debug.wait_layout()`, otherwise layout changes are not reported. + """ + if self.version >= (2, 3, 2): + # version check is necessary because otherwise we cannot reliably detect + # whether and where to wait for reply: + # - T1 reports unknown debuglink messages on the wirelink + # - TT < 2.3.0 does not reply to unknown debuglink messages due to a bug + self.debug.watch_layout(watch) def __enter__(self): # For usage in with/expected_responses @@ -265,60 +459,86 @@ def __enter__(self): def __exit__(self, _type, value, traceback): self.in_with_statement -= 1 - if _type is not None: - # Another exception raised - return False + # Clear input flow. + try: + if _type is not None: + # Another exception raised + return False - if self.expected_responses is None: - # no need to check anything else - return False + if self.expected_responses is None: + # no need to check anything else + return False - # return isinstance(value, TypeError) - # Evaluate missed responses in 'with' statement - if self.current_response < len(self.expected_responses): - self._raise_unexpected_response(None) + # Evaluate missed responses in 'with' statement + if self.current_response < len(self.expected_responses): + self._raise_unexpected_response(None) + + finally: + # Cleanup + self.expected_responses = None + self.current_response = None + self.ui.clear() + self.watch_layout(False) - # Cleanup - self.expected_responses = None - self.current_response = None return False def set_expected_responses(self, expected): + """Set a sequence of expected responses to client calls. + + Within a given with-block, the list of received responses from device must + match the list of expected responses, otherwise an AssertionError is raised. + + If an expected response is given a field value other than None, that field value + must exactly match the received field value. If a given field is None + (or unspecified) in the expected response, the received field value is not + checked. + + Each expected response can also be a tuple (bool, message). In that case, the + expected response is only evaluated if the first field is True. + This is useful for differentiating sequences between Trezor models: + + >>> trezor_one = client.features.model == "1" + >>> client.set_expected_responses([ + >>> messages.ButtonRequest(code=ConfirmOutput), + >>> (trezor_one, messages.ButtonRequest(code=ConfirmOutput)), + >>> messages.Success(), + >>> ]) + """ if not self.in_with_statement: raise RuntimeError("Must be called inside 'with' statement") - self.expected_responses = expected + + # make sure all items are (bool, message) tuples + expected_with_validity = ( + e if isinstance(e, tuple) else (True, e) for e in expected + ) + + # only apply those items that are (True, message) + self.expected_responses = [ + MessageFilter.from_message_or_type(expected) + for valid, expected in expected_with_validity + if valid + ] + self.current_response = 0 - def setup_debuglink(self, button, pin_correct): - # self.button = button # True -> YES button, False -> NO button - if pin_correct: - self.ui.pin = None - else: - self.ui.pin = "444222" + def use_pin_sequence(self, pins): + """Respond to PIN prompts from device with the provided PINs. + The sequence must be at least as long as the expected number of PIN prompts. + """ + self.ui.pins = iter(pins) - def set_passphrase(self, passphrase): + def use_passphrase(self, passphrase): + """Respond to passphrase prompts from device with the provided passphrase.""" self.ui.passphrase = Mnemonic.normalize_string(passphrase) - def set_mnemonic(self, mnemonic): + def use_mnemonic(self, mnemonic): + """Use the provided mnemonic to respond to device. + Only applies to T1, where device prompts the host for mnemonic words.""" self.mnemonic = Mnemonic.normalize_string(mnemonic).split(" ") def _raw_read(self): __tracebackhide__ = True # for pytest # pylint: disable=W0612 - # if SCREENSHOT and self.debug: - # from PIL import Image - - # layout = self.debug.state().layout - # im = Image.new("RGB", (128, 64)) - # pix = im.load() - # for x in range(128): - # for y in range(64): - # rx, ry = 127 - x, 63 - y - # if (ord(layout[rx + (ry / 8) * 128]) & (1 << (ry % 8))) > 0: - # pix[x, y] = (255, 255, 255) - # im.save("scr%05d.png" % self.screenshot_id) - # self.screenshot_id += 1 - resp = super()._raw_read() resp = self._filter_message(resp) self._check_request(resp) @@ -342,23 +562,7 @@ def _raise_unexpected_response(self, msg): for i in range(start_at, stop_at): exp = self.expected_responses[i] prefix = " " if i != self.current_response else ">>> " - set_fields = { - key: value - for key, value in exp.__dict__.items() - if value is not None and value != [] - } - oneline_str = ", ".join("{}={!r}".format(*i) for i in set_fields.items()) - if len(oneline_str) < 60: - output.append( - "{}{}({})".format(prefix, exp.__class__.__name__, oneline_str) - ) - else: - item = [] - item.append("{}{}(".format(prefix, exp.__class__.__name__)) - for key, value in set_fields.items(): - item.append("{} {}={!r}".format(prefix, key, value)) - item.append("{})".format(prefix)) - output.append("\n".join(item)) + output.append(textwrap.indent(exp.format(), prefix)) if stop_at < len(self.expected_responses): omitted = len(self.expected_responses) - stop_at output.append(" (...{} following responses omitted)".format(omitted)) @@ -366,7 +570,7 @@ def _raise_unexpected_response(self, msg): output.append("") if msg is not None: output.append("Actually received:") - output.append(protobuf.format_message(msg)) + output.append(textwrap.indent(protobuf.format_message(msg), " ")) else: output.append("This message was never received.") raise AssertionError("\n".join(output)) @@ -384,15 +588,9 @@ def _check_request(self, msg): expected = self.expected_responses[self.current_response] - if msg.__class__ != expected.__class__: + if not expected.match(msg): self._raise_unexpected_response(msg) - for field, value in expected.__dict__.items(): - if value is None or value == []: - continue - if getattr(msg, field) != value: - self._raise_unexpected_response(msg) - self.current_response += 1 def mnemonic_callback(self, _): @@ -405,30 +603,22 @@ def mnemonic_callback(self, _): raise RuntimeError("Unexpected call") -@expect(proto.Success, field="message") -def load_device_by_mnemonic( +@expect(messages.Success, field="message") +def load_device( client, mnemonic, pin, passphrase_protection, label, - language="english", + language="en-US", skip_checksum=False, - expand=False, + needs_backup=False, + no_backup=False, ): - # Convert mnemonic to UTF8 NKFD - mnemonic = Mnemonic.normalize_string(mnemonic) - - # Convert mnemonic to ASCII stream - mnemonic = mnemonic.encode() + if not isinstance(mnemonic, (list, tuple)): + mnemonic = [mnemonic] - m = Mnemonic("english") - - if expand: - mnemonic = m.expand(mnemonic) - - if not skip_checksum and not m.check(mnemonic): - raise ValueError("Invalid mnemonic checksum") + mnemonics = [Mnemonic.normalize_string(m) for m in mnemonic] if client.features.initialized: raise RuntimeError( @@ -436,76 +626,47 @@ def load_device_by_mnemonic( ) resp = client.call( - proto.LoadDevice( - mnemonic=mnemonic, + messages.LoadDevice( + mnemonics=mnemonics, pin=pin, passphrase_protection=passphrase_protection, language=language, label=label, skip_checksum=skip_checksum, + needs_backup=needs_backup, + no_backup=no_backup, ) ) client.init_device() return resp -@expect(proto.Success, field="message") -def load_device_by_xprv(client, xprv, pin, passphrase_protection, label, language): - if client.features.initialized: - raise RuntimeError( - "Device is initialized already. Call wipe_device() and try again." - ) - - if xprv[0:4] not in ("xprv", "tprv"): - raise ValueError("Unknown type of xprv") - - if not 100 < len(xprv) < 112: # yes this is correct in Python - raise ValueError("Invalid length of xprv") +# keep the old name for compatibility +load_device_by_mnemonic = load_device - node = proto.HDNodeType() - data = tools.b58decode(xprv, None).hex() - if data[90:92] != "00": - raise ValueError("Contain invalid private key") - - checksum = (tools.btc_hash(bytes.fromhex(data[:156]))[:4]).hex() - if checksum != data[156:]: - raise ValueError("Checksum doesn't match") - - # version 0488ade4 - # depth 00 - # fingerprint 00000000 - # child_num 00000000 - # chaincode 873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508 - # privkey 00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35 - # checksum e77e9d71 - - node.depth = int(data[8:10], 16) - node.fingerprint = int(data[10:18], 16) - node.child_num = int(data[18:26], 16) - node.chain_code = bytes.fromhex(data[26:90]) - node.private_key = bytes.fromhex(data[92:156]) # skip 0x00 indicating privkey - - resp = client.call( - proto.LoadDevice( - node=node, - pin=pin, - passphrase_protection=passphrase_protection, - language=language, - label=label, - ) - ) - client.init_device() - return resp - - -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") def self_test(client): if client.features.bootloader_mode is not True: raise RuntimeError("Device must be in bootloader mode") return client.call( - proto.SelfTest( + messages.SelfTest( payload=b"\x00\xFF\x55\xAA\x66\x99\x33\xCCABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\x00\xFF\x55\xAA\x66\x99\x33\xCC" ) ) + + +@expect(messages.Success, field="message") +def show_text(client, header_text, body_text, icon=None, icon_color=None): + body_text = [ + messages.DebugLinkShowTextItem(style=style, content=content) + for style, content in body_text + ] + msg = messages.DebugLinkShowText( + header_text=header_text, + body_text=body_text, + header_icon=icon, + icon_color=icon_color, + ) + return client.call(msg) diff --git a/hwilib/devices/trezorlib/device.py b/hwilib/devices/trezorlib/device.py index 9605bd128..2179cd19b 100644 --- a/hwilib/devices/trezorlib/device.py +++ b/hwilib/devices/trezorlib/device.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,40 +14,18 @@ # You should have received a copy of the License along with this library. # If not, see . -import logging import os import time -import warnings -from . import messages as proto +from . import messages from .exceptions import Cancelled from .tools import expect, session -from .transport import enumerate_devices, get_transport RECOVERY_BACK = "\x08" # backspace character, sent literally -LOG = logging.getLogger(__name__) - -class TrezorDevice: - """ - This class is deprecated. (There is no reason for it to exist in the first - place, it is nothing but a collection of two functions.) - Instead, please use functions from the ``trezorlib.transport`` module. - """ - - @classmethod - def enumerate(cls): - warnings.warn("TrezorDevice is deprecated.", DeprecationWarning) - return enumerate_devices() - - @classmethod - def find_by_path(cls, path): - warnings.warn("TrezorDevice is deprecated.", DeprecationWarning) - return get_transport(path, prefix_search=False) - - -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") +@session def apply_settings( client, label=None, @@ -57,59 +35,76 @@ def apply_settings( passphrase_always_on_device=None, auto_lock_delay_ms=None, display_rotation=None, + safety_checks=None, + experimental_features=None, ): - settings = proto.ApplySettings() - if label is not None: - settings.label = label - if language: - settings.language = language - if use_passphrase is not None: - settings.use_passphrase = use_passphrase - if homescreen is not None: - settings.homescreen = homescreen - if passphrase_always_on_device is not None: - settings.passphrase_always_on_device = passphrase_always_on_device - if auto_lock_delay_ms is not None: - settings.auto_lock_delay_ms = auto_lock_delay_ms - if display_rotation is not None: - settings.display_rotation = display_rotation + settings = messages.ApplySettings( + label=label, + language=language, + use_passphrase=use_passphrase, + homescreen=homescreen, + passphrase_always_on_device=passphrase_always_on_device, + auto_lock_delay_ms=auto_lock_delay_ms, + display_rotation=display_rotation, + safety_checks=safety_checks, + experimental_features=experimental_features, + ) out = client.call(settings) - client.init_device() # Reload Features + client.refresh_features() return out -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") +@session def apply_flags(client, flags): - out = client.call(proto.ApplyFlags(flags=flags)) - client.init_device() # Reload Features + out = client.call(messages.ApplyFlags(flags=flags)) + client.refresh_features() return out -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") +@session def change_pin(client, remove=False): - ret = client.call(proto.ChangePin(remove=remove)) - client.init_device() # Re-read features + ret = client.call(messages.ChangePin(remove=remove)) + client.refresh_features() return ret -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") +@session +def change_wipe_code(client, remove=False): + ret = client.call(messages.ChangeWipeCode(remove=remove)) + client.refresh_features() + return ret + + +@expect(messages.Success, field="message") +@session +def sd_protect(client, operation): + ret = client.call(messages.SdProtect(operation=operation)) + client.refresh_features() + return ret + + +@expect(messages.Success, field="message") +@session def wipe(client): - ret = client.call(proto.WipeDevice()) + ret = client.call(messages.WipeDevice()) client.init_device() return ret -@expect(proto.Success, field="message") +@session def recover( client, word_count=24, passphrase_protection=False, pin_protection=True, label=None, - language="english", + language="en-US", input_callback=None, - type=proto.RecoveryDeviceType.ScrambledWords, + type=messages.RecoveryDeviceType.ScrambledWords, dry_run=False, u2f_counter=None, ): @@ -127,32 +122,32 @@ def recover( if u2f_counter is None: u2f_counter = int(time.time()) - res = client.call( - proto.RecoveryDevice( - word_count=word_count, - passphrase_protection=bool(passphrase_protection), - pin_protection=bool(pin_protection), - label=label, - language=language, - enforce_wordlist=True, - type=type, - dry_run=dry_run, - u2f_counter=u2f_counter, - ) + msg = messages.RecoveryDevice( + word_count=word_count, enforce_wordlist=True, type=type, dry_run=dry_run ) - while isinstance(res, proto.WordRequest): + if not dry_run: + # set additional parameters + msg.passphrase_protection = passphrase_protection + msg.pin_protection = pin_protection + msg.label = label + msg.language = language + msg.u2f_counter = u2f_counter + + res = client.call(msg) + + while isinstance(res, messages.WordRequest): try: inp = input_callback(res.type) - res = client.call(proto.WordAck(word=inp)) + res = client.call(messages.WordAck(word=inp)) except Cancelled: - res = client.call(proto.Cancel()) + res = client.call(messages.Cancel()) client.init_device() return res -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") @session def reset( client, @@ -161,10 +156,11 @@ def reset( passphrase_protection=False, pin_protection=True, label=None, - language="english", - # u2f_counter=0, - # skip_backup=False, - # no_backup=False, + language="en-US", + u2f_counter=0, + skip_backup=False, + no_backup=False, + backup_type=messages.BackupType.Bip39, ): if client.features.initialized: raise RuntimeError( @@ -178,30 +174,38 @@ def reset( strength = 128 # Begin with device reset workflow - msg = proto.ResetDevice( + msg = messages.ResetDevice( display_random=bool(display_random), strength=strength, passphrase_protection=bool(passphrase_protection), pin_protection=bool(pin_protection), language=language, label=label, - # u2f_counter=u2f_counter, - # skip_backup=bool(skip_backup), - # no_backup=bool(no_backup), + u2f_counter=u2f_counter, + skip_backup=bool(skip_backup), + no_backup=bool(no_backup), + backup_type=backup_type, ) resp = client.call(msg) - if not isinstance(resp, proto.EntropyRequest): + if not isinstance(resp, messages.EntropyRequest): raise RuntimeError("Invalid response, expected EntropyRequest") external_entropy = os.urandom(32) - LOG.debug("Computer generated entropy: " + external_entropy.hex()) - ret = client.call(proto.EntropyAck(entropy=external_entropy)) + # LOG.debug("Computer generated entropy: " + external_entropy.hex()) + ret = client.call(messages.EntropyAck(entropy=external_entropy)) client.init_device() return ret -@expect(proto.Success, field="message") +@expect(messages.Success, field="message") +@session def backup(client): - ret = client.call(proto.BackupDevice()) + ret = client.call(messages.BackupDevice()) + client.refresh_features() return ret + + +@expect(messages.Success, field="message") +def cancel_authorization(client): + return client.call(messages.CancelAuthorization()) diff --git a/hwilib/devices/trezorlib/exceptions.py b/hwilib/devices/trezorlib/exceptions.py index f95271eef..a2aa9bc71 100644 --- a/hwilib/devices/trezorlib/exceptions.py +++ b/hwilib/devices/trezorlib/exceptions.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -22,8 +22,9 @@ class TrezorException(Exception): class TrezorFailure(TrezorException): def __init__(self, failure): self.failure = failure - # TODO: this is backwards compatibility with tests. it should be changed - super().__init__(self.failure.code, self.failure.message) + self.code = failure.code + self.message = failure.message + super().__init__(self.code, self.message, self.failure) def __str__(self): from .messages import FailureType @@ -33,8 +34,8 @@ def __str__(self): for name in dir(FailureType) if not name.startswith("_") } - if self.failure.message is not None: - return "{}: {}".format(types[self.failure.code], self.failure.message) + if self.message is not None: + return "{}: {}".format(types[self.code], self.message) else: return types[self.failure.code] diff --git a/hwilib/devices/trezorlib/firmware.py b/hwilib/devices/trezorlib/firmware.py index e80a5af55..bab446d05 100644 --- a/hwilib/devices/trezorlib/firmware.py +++ b/hwilib/devices/trezorlib/firmware.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -16,31 +16,61 @@ import hashlib from enum import Enum -from typing import NewType, Tuple +from typing import Callable, List, Tuple import construct as c import ecdsa -import pyblake2 from . import cosi, messages, tools +try: + from hashlib import blake2s +except ImportError: + from pyblake2 import blake2s + + V1_SIGNATURE_SLOTS = 3 -V1_BOOTLOADER_KEYS = { - 1: "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58", - 2: "0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1", - 3: "0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58", - 4: "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a", - 5: "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45", -} +V1_BOOTLOADER_KEYS = [ + bytes.fromhex(key) + for key in ( + "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58", + "0463279c0c0866e50c05c799d32bd6bab0188b6de06536d1109d2ed9ce76cb335c490e55aee10cc901215132e853097d5432eda06b792073bd7740c94ce4516cb1", + "0443aedbb6f7e71c563f8ed2ef64ec9981482519e7ef4f4aa98b27854e8c49126d4956d300ab45fdc34cd26bc8710de0a31dbdf6de7435fd0b492be70ac75fde58", + "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a", + "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45", + ) +] + +V2_BOARDLOADER_KEYS = [ + bytes.fromhex(key) + for key in ( + "0eb9856be9ba7e972c7f34eac1ed9b6fd0efd172ec00faf0c589759da4ddfba0", + "ac8ab40b32c98655798fd5da5e192be27a22306ea05c6d277cdff4a3f4125cd8", + "ce0fcd12543ef5936cf2804982136707863d17295faced72af171d6e6513ff06", + ) +] + +V2_BOARDLOADER_DEV_KEYS = [ + bytes.fromhex(key) + for key in ( + "db995fe25169d141cab9bbba92baa01f9f2e1ece7df4cb2ac05190f37fcc1f9d", + "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12", + "22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6", + ) +] V2_BOOTLOADER_KEYS = [ - bytes.fromhex("c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f"), - bytes.fromhex("80d036b08739b846f4cb77593078deb25dc9487aedcf52e30b4fb7cd7024178a"), - bytes.fromhex("b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751"), + bytes.fromhex(key) + for key in ( + "c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f", + "80d036b08739b846f4cb77593078deb25dc9487aedcf52e30b4fb7cd7024178a", + "b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751", + ) ] -V2_BOOTLOADER_M = 2 -V2_BOOTLOADER_N = 3 +V2_SIGS_REQUIRED = 2 + +ONEV2_CHUNK_SIZE = 1024 * 64 V2_CHUNK_SIZE = 1024 * 128 @@ -57,10 +87,47 @@ def _transform_vendor_trust(data: bytes) -> bytes: return bytes(~b & 0xFF for b in data)[::-1] +class FirmwareIntegrityError(Exception): + pass + + +class InvalidSignatureError(FirmwareIntegrityError): + pass + + +class Unsigned(FirmwareIntegrityError): + pass + + +class ToifMode(Enum): + full_color = b"f" + grayscale = b"g" + + +class HeaderType(Enum): + FIRMWARE = b"TRZF" + BOOTLOADER = b"TRZB" + + +class EnumAdapter(c.Adapter): + def __init__(self, subcon, enum): + self.enum = enum + super().__init__(subcon) + + def _encode(self, obj, ctx, path): + return obj.value + + def _decode(self, obj, ctx, path): + try: + return self.enum(obj) + except ValueError: + return obj + + # fmt: off Toif = c.Struct( "magic" / c.Const(b"TOI"), - "format" / c.Enum(c.Byte, full_color=b"f", grayscale=b"g"), + "format" / EnumAdapter(c.Bytes(1), ToifMode), "width" / c.Int16ul, "height" / c.Int16ul, "data" / c.Prefixed(c.Int32ul, c.GreedyBytes), @@ -68,7 +135,7 @@ def _transform_vendor_trust(data: bytes) -> bytes: VendorTrust = c.Transformed(c.BitStruct( - "reserved" / c.Default(c.BitsInteger(9), 0), + "_reserved" / c.Default(c.BitsInteger(9), 0), "show_vendor_string" / c.Flag, "require_user_click" / c.Flag, "red_background" / c.Flag, @@ -79,30 +146,27 @@ def _transform_vendor_trust(data: bytes) -> bytes: VendorHeader = c.Struct( "_start_offset" / c.Tell, "magic" / c.Const(b"TRZV"), - "_header_len" / c.Padding(4), + "header_len" / c.Int32ul, "expiry" / c.Int32ul, "version" / c.Struct( "major" / c.Int8ul, "minor" / c.Int8ul, ), - "vendor_sigs_required" / c.Int8ul, - "vendor_sigs_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)), - "vendor_trust" / VendorTrust, - "reserved" / c.Padding(14), - "pubkeys" / c.Bytes(32)[c.this.vendor_sigs_n], - "vendor_string" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")), - "vendor_image" / Toif, - "_data_end_offset" / c.Tell, - - c.Padding(-(c.this._data_end_offset + 65) % 512), + "sig_m" / c.Int8ul, + "sig_n" / c.Rebuild(c.Int8ul, c.len_(c.this.pubkeys)), + "trust" / VendorTrust, + "_reserved" / c.Padding(14), + "pubkeys" / c.Bytes(32)[c.this.sig_n], + "text" / c.Aligned(4, c.PascalString(c.Int8ul, "utf-8")), + "image" / Toif, + "_end_offset" / c.Tell, + + "_min_header_len" / c.Check(c.this.header_len > (c.this._end_offset - c.this._start_offset) + 65), + "_header_len_aligned" / c.Check(c.this.header_len % 512 == 0), + + c.Padding(c.this.header_len - c.this._end_offset + c.this._start_offset - 65), "sigmask" / c.Byte, "signature" / c.Bytes(64), - - "_end_offset" / c.Tell, - "header_len" / c.Pointer( - c.this._start_offset + 4, - c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset) - ), ) @@ -116,8 +180,8 @@ def _transform_vendor_trust(data: bytes) -> bytes: FirmwareHeader = c.Struct( "_start_offset" / c.Tell, - "magic" / c.Const(b"TRZF"), - "_header_len" / c.Padding(4), + "magic" / EnumAdapter(c.Bytes(4), HeaderType), + "header_len" / c.Int32ul, "expiry" / c.Int32ul, "code_length" / c.Rebuild( c.Int32ul, @@ -127,31 +191,59 @@ def _transform_vendor_trust(data: bytes) -> bytes: ), "version" / VersionLong, "fix_version" / VersionLong, - "reserved" / c.Padding(8), + "_reserved" / c.Padding(8), "hashes" / c.Bytes(32)[16], - "reserved" / c.Padding(415), + "v1_signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS], + "v1_key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136 + + "_reserved" / c.Padding(220), "sigmask" / c.Byte, "signature" / c.Bytes(64), "_end_offset" / c.Tell, - "header_len" / c.Pointer( - c.this._start_offset + 4, - c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset) + + "_rebuild_header_len" / c.If( + c.this.version.major > 1, + c.Pointer( + c.this._start_offset + 4, + c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset) + ), ), ) -Firmware = c.Struct( - "vendor_header" / VendorHeader, - "firmware_header" / FirmwareHeader, +"""Raw firmware image. + +Consists of firmware header and code block. +This is the expected format of firmware binaries for Trezor One, or bootloader images +for Trezor T.""" +FirmwareImage = c.Struct( + "header" / FirmwareHeader, "_code_offset" / c.Tell, - "code" / c.Bytes(c.this.firmware_header.code_length), + "code" / c.Bytes(c.this.header.code_length), c.Terminated, ) -FirmwareV1 = c.Struct( +"""Firmware image prefixed by a vendor header. + +This is the expected format of firmware binaries for Trezor T.""" +VendorFirmware = c.Struct( + "vendor_header" / VendorHeader, + "image" / FirmwareImage, + c.Terminated, +) + + +"""Legacy firmware image. +Consists of a custom header and code block. +This is the expected format of firmware binaries for Trezor One pre-1.8.0. + +The code block can optionally be interpreted as a new-style firmware image. That is the +expected format of firmware binary for Trezor One version 1.8.0, which can be installed +by both the older and the newer bootloader.""" +LegacyFirmware = c.Struct( "magic" / c.Const(b"TRZR"), "code_length" / c.Rebuild(c.Int32ul, c.len_(c.this.code)), "key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136 @@ -159,10 +251,12 @@ def _transform_vendor_trust(data: bytes) -> bytes: c.Padding(7), "restore_storage" / c.Flag, ), - "reserved" / c.Padding(52), + "_reserved" / c.Padding(52), "signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS], "code" / c.Bytes(c.this.code_length), c.Terminated, + + "embedded_onev2" / c.RestreamData(c.this.code, c.Optional(FirmwareImage)), ) # fmt: on @@ -171,86 +265,176 @@ def _transform_vendor_trust(data: bytes) -> bytes: class FirmwareFormat(Enum): TREZOR_ONE = 1 TREZOR_T = 2 + TREZOR_ONE_V2 = 3 -FirmwareType = NewType("FirmwareType", c.Container) -ParsedFirmware = Tuple[FirmwareFormat, FirmwareType] +ParsedFirmware = Tuple[FirmwareFormat, c.Container] def parse(data: bytes) -> ParsedFirmware: if data[:4] == b"TRZR": version = FirmwareFormat.TREZOR_ONE - cls = FirmwareV1 + cls = LegacyFirmware elif data[:4] == b"TRZV": version = FirmwareFormat.TREZOR_T - cls = Firmware + cls = VendorFirmware + elif data[:4] == b"TRZF": + version = FirmwareFormat.TREZOR_ONE_V2 + cls = FirmwareImage else: raise ValueError("Unrecognized firmware image type") try: fw = cls.parse(data) except Exception as e: - raise ValueError("Invalid firmware image") from e - return version, FirmwareType(fw) + raise FirmwareIntegrityError("Invalid firmware image") from e + return version, fw -def digest_v1(fw: FirmwareType) -> bytes: +def digest_onev1(fw: c.Container) -> bytes: return hashlib.sha256(fw.code).digest() -def check_sig_v1(fw: FirmwareType, idx: int) -> bool: - key_idx = fw.key_indexes[idx] - signature = fw.signatures[idx] +def check_sig_v1( + digest: bytes, key_indexes: List[int], signatures: List[bytes] +) -> None: + distinct_key_indexes = set(i for i in key_indexes if i != 0) + if not distinct_key_indexes: + raise Unsigned - if key_idx == 0: - # no signature = invalid signature - return False + if len(distinct_key_indexes) < len(key_indexes): + raise InvalidSignatureError( + "Not enough distinct signatures (found {}, need {})".format( + len(distinct_key_indexes), len(key_indexes) + ) + ) - if key_idx not in V1_BOOTLOADER_KEYS: - # unknown pubkey - return False + for i in range(len(key_indexes)): + key_idx = key_indexes[i] - 1 + signature = signatures[i] - pubkey = bytes.fromhex(V1_BOOTLOADER_KEYS[key_idx])[1:] - verify = ecdsa.VerifyingKey.from_string( - pubkey, curve=ecdsa.curves.SECP256k1, hashfunc=hashlib.sha256 - ) - try: - verify.verify(signature, fw.code) - return True - except ecdsa.BadSignatureError: - return False + if key_idx >= len(V1_BOOTLOADER_KEYS): + # unknown pubkey + raise InvalidSignatureError("Unknown key in slot {}".format(i)) + + pubkey = V1_BOOTLOADER_KEYS[key_idx][1:] + verify = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1) + try: + verify.verify_digest(signature, digest) + except ecdsa.BadSignatureError as e: + raise InvalidSignatureError("Invalid signature in slot {}".format(i)) from e -def _header_digest(header: c.Container, header_type: c.Construct) -> bytes: +def header_digest(header: c.Container, hash_function: Callable = blake2s) -> bytes: stripped_header = header.copy() stripped_header.sigmask = 0 stripped_header.signature = b"\0" * 64 + stripped_header.v1_key_indexes = [0, 0, 0] + stripped_header.v1_signatures = [b"\0" * 64] * 3 + if header.magic == b"TRZV": + header_type = VendorHeader + else: + header_type = FirmwareHeader header_bytes = header_type.build(stripped_header) - return pyblake2.blake2s(header_bytes).digest() + return hash_function(header_bytes).digest() + + +def digest_v2(fw: c.Container) -> bytes: + return header_digest(fw.image.header, blake2s) + +def digest_onev2(fw: c.Container) -> bytes: + return header_digest(fw.header, hashlib.sha256) -def digest(fw: FirmwareType) -> bytes: - return _header_digest(fw.firmware_header, FirmwareHeader) +def calculate_code_hashes( + code: bytes, + code_offset: int, + hash_function: Callable = blake2s, + chunk_size: int = V2_CHUNK_SIZE, + padding_byte: bytes = None, +) -> None: + hashes = [] + # End offset for each chunk. Normally this would be (i+1)*chunk_size for i-th chunk, + # but the first chunk is shorter by code_offset, so all end offsets are shifted. + ends = [(i + 1) * chunk_size - code_offset for i in range(16)] + start = 0 + for end in ends: + chunk = code[start:end] + # padding for last non-empty chunk + if padding_byte is not None and start < len(code) and end > len(code): + chunk += padding_byte[0:1] * (end - start - len(chunk)) -def validate(fw: FirmwareType, skip_vendor_header=False) -> bool: - vendor_fingerprint = _header_digest(fw.vendor_header, VendorHeader) - fingerprint = digest(fw) + if not chunk: + hashes.append(b"\0" * 32) + else: + hashes.append(hash_function(chunk).digest()) + + start = end + + return hashes + + +def validate_code_hashes(fw: c.Container, version: FirmwareFormat) -> None: + if version == FirmwareFormat.TREZOR_ONE_V2: + image = fw + hash_function = hashlib.sha256 + chunk_size = ONEV2_CHUNK_SIZE + padding_byte = b"\xff" + else: + image = fw.image + hash_function = blake2s + chunk_size = V2_CHUNK_SIZE + padding_byte = None + + expected_hashes = calculate_code_hashes( + image.code, image._code_offset, hash_function, chunk_size, padding_byte + ) + if expected_hashes != image.header.hashes: + raise FirmwareIntegrityError("Invalid firmware data.") + + +def validate_onev2(fw: c.Container, allow_unsigned: bool = False) -> None: + try: + check_sig_v1( + digest_onev2(fw), + fw.header.v1_key_indexes, + fw.header.v1_signatures, + ) + except Unsigned: + if not allow_unsigned: + raise + + validate_code_hashes(fw, FirmwareFormat.TREZOR_ONE_V2) + + +def validate_onev1(fw: c.Container, allow_unsigned: bool = False) -> None: + try: + check_sig_v1(digest_onev1(fw), fw.key_indexes, fw.signatures) + except Unsigned: + if not allow_unsigned: + raise + if fw.embedded_onev2: + validate_onev2(fw.embedded_onev2, allow_unsigned) + + +def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None: + vendor_fingerprint = header_digest(fw.vendor_header) + fingerprint = digest_v2(fw) if not skip_vendor_header: try: # if you want to validate a custom vendor header, you can modify # the global variables to match your keys and m-of-n scheme - cosi.verify_m_of_n( + cosi.verify( fw.vendor_header.signature, vendor_fingerprint, - V2_BOOTLOADER_M, - V2_BOOTLOADER_N, - fw.vendor_header.sigmask, + V2_SIGS_REQUIRED, V2_BOOTLOADER_KEYS, + fw.vendor_header.sigmask, ) except Exception: - raise ValueError("Invalid vendor header signature.") + raise InvalidSignatureError("Invalid vendor header signature.") # XXX expiry is not used now # now = time.gmtime() @@ -258,37 +442,44 @@ def validate(fw: FirmwareType, skip_vendor_header=False) -> bool: # raise ValueError("Vendor header expired.") try: - cosi.verify_m_of_n( - fw.firmware_header.signature, + cosi.verify( + fw.image.header.signature, fingerprint, - fw.vendor_header.vendor_sigs_required, - fw.vendor_header.vendor_sigs_n, - fw.firmware_header.sigmask, + fw.vendor_header.sig_m, fw.vendor_header.pubkeys, + fw.image.header.sigmask, ) except Exception: - raise ValueError("Invalid firmware signature.") + raise InvalidSignatureError("Invalid firmware signature.") # XXX expiry is not used now - # if time.gmtime(fw.firmware_header.expiry) < now: + # if time.gmtime(fw.image.header.expiry) < now: # raise ValueError("Firmware header expired.") + validate_code_hashes(fw, FirmwareFormat.TREZOR_T) - for i, expected_hash in enumerate(fw.firmware_header.hashes): - if i == 0: - # Because first chunk is sent along with headers, there is less code in it. - chunk = fw.code[: V2_CHUNK_SIZE - fw._code_offset] - else: - # Subsequent chunks are shifted by the "missing header" size. - ptr = i * V2_CHUNK_SIZE - fw._code_offset - chunk = fw.code[ptr : ptr + V2_CHUNK_SIZE] - - if not chunk and expected_hash == b"\0" * 32: - continue - chunk_hash = pyblake2.blake2s(chunk).digest() - if chunk_hash != expected_hash: - raise ValueError("Invalid firmware data.") - return True +def digest(version: FirmwareFormat, fw: c.Container) -> bytes: + if version == FirmwareFormat.TREZOR_ONE: + return digest_onev1(fw) + elif version == FirmwareFormat.TREZOR_ONE_V2: + return digest_onev2(fw) + elif version == FirmwareFormat.TREZOR_T: + return digest_v2(fw) + else: + raise ValueError("Unrecognized firmware version") + + +def validate( + version: FirmwareFormat, fw: c.Container, allow_unsigned: bool = False +) -> None: + if version == FirmwareFormat.TREZOR_ONE: + return validate_onev1(fw, allow_unsigned) + elif version == FirmwareFormat.TREZOR_ONE_V2: + return validate_onev2(fw, allow_unsigned) + elif version == FirmwareFormat.TREZOR_T: + return validate_v2(fw) + else: + raise ValueError("Unrecognized firmware version") # ====== Client functions ====== # @@ -312,7 +503,7 @@ def update(client, data): # TREZORv2 method while isinstance(resp, messages.FirmwareRequest): payload = data[resp.offset : resp.offset + resp.length] - digest = pyblake2.blake2s(payload).digest() + digest = blake2s(payload).digest() resp = client.call(messages.FirmwareUpload(payload=payload, hash=digest)) if isinstance(resp, messages.Success): diff --git a/hwilib/devices/trezorlib/log.py b/hwilib/devices/trezorlib/log.py index 50f778a12..696a150c3 100644 --- a/hwilib/devices/trezorlib/log.py +++ b/hwilib/devices/trezorlib/log.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -19,7 +19,13 @@ from . import protobuf -OMITTED_MESSAGES = set() # type: Set[Type[protobuf.MessageType]] +OMITTED_MESSAGES: Set[Type[protobuf.MessageType]] = set() + +DUMP_BYTES = 5 +DUMP_PACKETS = 4 + +logging.addLevelName(DUMP_BYTES, "BYTES") +logging.addLevelName(DUMP_PACKETS, "PACKETS") class PrettyProtobufFormatter(logging.Formatter): @@ -39,13 +45,20 @@ def format(self, record: logging.LogRecord) -> str: return message -def enable_debug_output(handler: Optional[logging.Handler] = None): +def enable_debug_output(verbosity: int = 1, handler: Optional[logging.Handler] = None): if handler is None: handler = logging.StreamHandler() formatter = PrettyProtobufFormatter() handler.setFormatter(formatter) + if verbosity > 0: + level = logging.DEBUG + if verbosity > 1: + level = DUMP_BYTES + if verbosity > 2: + level = DUMP_PACKETS + logger = logging.getLogger("trezorlib") - logger.setLevel(logging.DEBUG) + logger.setLevel(level) logger.addHandler(handler) diff --git a/hwilib/devices/trezorlib/mapping.py b/hwilib/devices/trezorlib/mapping.py index 11c94cb06..a4e160d21 100644 --- a/hwilib/devices/trezorlib/mapping.py +++ b/hwilib/devices/trezorlib/mapping.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,7 +14,10 @@ # You should have received a copy of the License along with this library. # If not, see . -from . import messages +import io +from typing import Tuple + +from . import messages, protobuf map_type_to_class = {} map_class_to_type = {} @@ -25,6 +28,10 @@ def build_map(): if msg_name.startswith("__"): continue + if msg_name == "Literal": + # TODO: remove this when we have a good implementation of enums + continue + try: msg_class = getattr(messages, msg_name) except AttributeError: @@ -59,4 +66,17 @@ def get_class(t): return map_type_to_class[t] +def encode(msg: protobuf.MessageType) -> Tuple[int, bytes]: + message_type = msg.MESSAGE_WIRE_TYPE + buf = io.BytesIO() + protobuf.dump_message(buf, msg) + return message_type, buf.getvalue() + + +def decode(message_type: int, message_bytes: bytes) -> protobuf.MessageType: + cls = get_class(message_type) + buf = io.BytesIO(message_bytes) + return protobuf.load_message(buf, cls) + + build_map() diff --git a/hwilib/devices/trezorlib/messages/Address.py b/hwilib/devices/trezorlib/messages/Address.py index 8db78f462..f07b92c6b 100644 --- a/hwilib/devices/trezorlib/messages/Address.py +++ b/hwilib/devices/trezorlib/messages/Address.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Address(p.MessageType): MESSAGE_WIRE_TYPE = 30 def __init__( self, - address: str = None, + *, + address: str, ) -> None: self.address = address @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UnicodeType, 0), # required + 1: ('address', p.UnicodeType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/AmountUnit.py b/hwilib/devices/trezorlib/messages/AmountUnit.py new file mode 100644 index 000000000..e602f2dfe --- /dev/null +++ b/hwilib/devices/trezorlib/messages/AmountUnit.py @@ -0,0 +1,12 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +BITCOIN: Literal[0] = 0 +MILLIBITCOIN: Literal[1] = 1 +MICROBITCOIN: Literal[2] = 2 +SATOSHI: Literal[3] = 3 diff --git a/hwilib/devices/trezorlib/messages/ApplyFlags.py b/hwilib/devices/trezorlib/messages/ApplyFlags.py index 775e6dbfc..408b344c8 100644 --- a/hwilib/devices/trezorlib/messages/ApplyFlags.py +++ b/hwilib/devices/trezorlib/messages/ApplyFlags.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class ApplyFlags(p.MessageType): MESSAGE_WIRE_TYPE = 28 def __init__( self, + *, flags: int = None, ) -> None: self.flags = flags @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('flags', p.UVarintType, 0), + 1: ('flags', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/ApplySettings.py b/hwilib/devices/trezorlib/messages/ApplySettings.py index 26b39a288..40ed92cba 100644 --- a/hwilib/devices/trezorlib/messages/ApplySettings.py +++ b/hwilib/devices/trezorlib/messages/ApplySettings.py @@ -2,12 +2,21 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeSafetyCheckLevel = Literal[0, 1, 2] + except ImportError: + pass + class ApplySettings(p.MessageType): MESSAGE_WIRE_TYPE = 25 def __init__( self, + *, language: str = None, label: str = None, use_passphrase: bool = None, @@ -15,6 +24,8 @@ def __init__( auto_lock_delay_ms: int = None, display_rotation: int = None, passphrase_always_on_device: bool = None, + safety_checks: EnumTypeSafetyCheckLevel = None, + experimental_features: bool = None, ) -> None: self.language = language self.label = label @@ -23,15 +34,19 @@ def __init__( self.auto_lock_delay_ms = auto_lock_delay_ms self.display_rotation = display_rotation self.passphrase_always_on_device = passphrase_always_on_device + self.safety_checks = safety_checks + self.experimental_features = experimental_features @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('language', p.UnicodeType, 0), - 2: ('label', p.UnicodeType, 0), - 3: ('use_passphrase', p.BoolType, 0), - 4: ('homescreen', p.BytesType, 0), - 6: ('auto_lock_delay_ms', p.UVarintType, 0), - 7: ('display_rotation', p.UVarintType, 0), - 8: ('passphrase_always_on_device', p.BoolType, 0), + 1: ('language', p.UnicodeType, None), + 2: ('label', p.UnicodeType, None), + 3: ('use_passphrase', p.BoolType, None), + 4: ('homescreen', p.BytesType, None), + 6: ('auto_lock_delay_ms', p.UVarintType, None), + 7: ('display_rotation', p.UVarintType, None), + 8: ('passphrase_always_on_device', p.BoolType, None), + 9: ('safety_checks', p.EnumType("SafetyCheckLevel", (0, 1, 2)), None), + 10: ('experimental_features', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/AuthorizeCoinJoin.py b/hwilib/devices/trezorlib/messages/AuthorizeCoinJoin.py new file mode 100644 index 000000000..b68ac7f7a --- /dev/null +++ b/hwilib/devices/trezorlib/messages/AuthorizeCoinJoin.py @@ -0,0 +1,48 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + EnumTypeAmountUnit = Literal[0, 1, 2, 3] + except ImportError: + pass + + +class AuthorizeCoinJoin(p.MessageType): + MESSAGE_WIRE_TYPE = 51 + UNSTABLE = True + + def __init__( + self, + *, + coordinator: str, + max_total_fee: int, + address_n: List[int] = None, + fee_per_anonymity: int = None, + coin_name: str = "Bitcoin", + script_type: EnumTypeInputScriptType = 0, + amount_unit: EnumTypeAmountUnit = 0, + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.coordinator = coordinator + self.max_total_fee = max_total_fee + self.fee_per_anonymity = fee_per_anonymity + self.coin_name = coin_name + self.script_type = script_type + self.amount_unit = amount_unit + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('coordinator', p.UnicodeType, p.FLAG_REQUIRED), + 2: ('max_total_fee', p.UVarintType, p.FLAG_REQUIRED), + 3: ('fee_per_anonymity', p.UVarintType, None), + 4: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 5: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 6: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS + 11: ('amount_unit', p.EnumType("AmountUnit", (0, 1, 2, 3)), 0), # default=BITCOIN + } diff --git a/hwilib/devices/trezorlib/messages/BackupDevice.py b/hwilib/devices/trezorlib/messages/BackupDevice.py index 5acb2a6af..5464034ff 100644 --- a/hwilib/devices/trezorlib/messages/BackupDevice.py +++ b/hwilib/devices/trezorlib/messages/BackupDevice.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class BackupDevice(p.MessageType): MESSAGE_WIRE_TYPE = 34 diff --git a/hwilib/devices/trezorlib/messages/BackupType.py b/hwilib/devices/trezorlib/messages/BackupType.py new file mode 100644 index 000000000..d8ed8002f --- /dev/null +++ b/hwilib/devices/trezorlib/messages/BackupType.py @@ -0,0 +1,11 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Bip39: Literal[0] = 0 +Slip39_Basic: Literal[1] = 1 +Slip39_Advanced: Literal[2] = 2 diff --git a/hwilib/devices/trezorlib/messages/ButtonAck.py b/hwilib/devices/trezorlib/messages/ButtonAck.py index cb6e17067..48d50d806 100644 --- a/hwilib/devices/trezorlib/messages/ButtonAck.py +++ b/hwilib/devices/trezorlib/messages/ButtonAck.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class ButtonAck(p.MessageType): MESSAGE_WIRE_TYPE = 27 diff --git a/hwilib/devices/trezorlib/messages/ButtonRequest.py b/hwilib/devices/trezorlib/messages/ButtonRequest.py index ac53cae35..bbbc48550 100644 --- a/hwilib/devices/trezorlib/messages/ButtonRequest.py +++ b/hwilib/devices/trezorlib/messages/ButtonRequest.py @@ -2,21 +2,27 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeButtonRequestType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + except ImportError: + pass + class ButtonRequest(p.MessageType): MESSAGE_WIRE_TYPE = 26 def __init__( self, - code: int = None, - data: str = None, + *, + code: EnumTypeButtonRequestType = None, ) -> None: self.code = code - self.data = data @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('code', p.UVarintType, 0), - 2: ('data', p.UnicodeType, 0), + 1: ('code', p.EnumType("ButtonRequestType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)), None), } diff --git a/hwilib/devices/trezorlib/messages/ButtonRequestType.py b/hwilib/devices/trezorlib/messages/ButtonRequestType.py index 5f8decd08..535ec54fb 100644 --- a/hwilib/devices/trezorlib/messages/ButtonRequestType.py +++ b/hwilib/devices/trezorlib/messages/ButtonRequestType.py @@ -1,17 +1,28 @@ # Automatically generated by pb2py # fmt: off -Other = 1 -FeeOverThreshold = 2 -ConfirmOutput = 3 -ResetDevice = 4 -ConfirmWord = 5 -WipeDevice = 6 -ProtectCall = 7 -SignTx = 8 -FirmwareCheck = 9 -Address = 10 -PublicKey = 11 -MnemonicWordCount = 12 -MnemonicInput = 13 -PassphraseType = 14 -UnknownDerivationPath = 15 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Other: Literal[1] = 1 +FeeOverThreshold: Literal[2] = 2 +ConfirmOutput: Literal[3] = 3 +ResetDevice: Literal[4] = 4 +ConfirmWord: Literal[5] = 5 +WipeDevice: Literal[6] = 6 +ProtectCall: Literal[7] = 7 +SignTx: Literal[8] = 8 +FirmwareCheck: Literal[9] = 9 +Address: Literal[10] = 10 +PublicKey: Literal[11] = 11 +MnemonicWordCount: Literal[12] = 12 +MnemonicInput: Literal[13] = 13 +_Deprecated_ButtonRequest_PassphraseType: Literal[14] = 14 +UnknownDerivationPath: Literal[15] = 15 +RecoveryHomepage: Literal[16] = 16 +Success: Literal[17] = 17 +Warning: Literal[18] = 18 +PassphraseEntry: Literal[19] = 19 +PinEntry: Literal[20] = 20 diff --git a/hwilib/devices/trezorlib/messages/Cancel.py b/hwilib/devices/trezorlib/messages/Cancel.py index 29eb2ee2a..22ea2b900 100644 --- a/hwilib/devices/trezorlib/messages/Cancel.py +++ b/hwilib/devices/trezorlib/messages/Cancel.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Cancel(p.MessageType): MESSAGE_WIRE_TYPE = 20 diff --git a/hwilib/devices/trezorlib/messages/Capability.py b/hwilib/devices/trezorlib/messages/Capability.py new file mode 100644 index 000000000..8f83aafba --- /dev/null +++ b/hwilib/devices/trezorlib/messages/Capability.py @@ -0,0 +1,25 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Bitcoin: Literal[1] = 1 +Bitcoin_like: Literal[2] = 2 +Binance: Literal[3] = 3 +Cardano: Literal[4] = 4 +Crypto: Literal[5] = 5 +EOS: Literal[6] = 6 +Ethereum: Literal[7] = 7 +Lisk: Literal[8] = 8 +Monero: Literal[9] = 9 +NEM: Literal[10] = 10 +Ripple: Literal[11] = 11 +Stellar: Literal[12] = 12 +Tezos: Literal[13] = 13 +U2F: Literal[14] = 14 +Shamir: Literal[15] = 15 +ShamirGroups: Literal[16] = 16 +PassphraseEntry: Literal[17] = 17 diff --git a/hwilib/devices/trezorlib/messages/ChangePin.py b/hwilib/devices/trezorlib/messages/ChangePin.py index 8b2e82ea1..a2c4327d5 100644 --- a/hwilib/devices/trezorlib/messages/ChangePin.py +++ b/hwilib/devices/trezorlib/messages/ChangePin.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class ChangePin(p.MessageType): MESSAGE_WIRE_TYPE = 4 def __init__( self, + *, remove: bool = None, ) -> None: self.remove = remove @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('remove', p.BoolType, 0), + 1: ('remove', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/ClearSession.py b/hwilib/devices/trezorlib/messages/ClearSession.py deleted file mode 100644 index b929c2ac0..000000000 --- a/hwilib/devices/trezorlib/messages/ClearSession.py +++ /dev/null @@ -1,7 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -from .. import protobuf as p - - -class ClearSession(p.MessageType): - MESSAGE_WIRE_TYPE = 24 diff --git a/hwilib/devices/trezorlib/messages/DebugLinkDecision.py b/hwilib/devices/trezorlib/messages/DebugLinkDecision.py index 57be789a3..42be1e295 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkDecision.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkDecision.py @@ -2,24 +2,42 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeDebugSwipeDirection = Literal[0, 1, 2, 3] + except ImportError: + pass + class DebugLinkDecision(p.MessageType): MESSAGE_WIRE_TYPE = 100 def __init__( self, + *, yes_no: bool = None, - up_down: bool = None, + swipe: EnumTypeDebugSwipeDirection = None, input: str = None, + x: int = None, + y: int = None, + wait: bool = None, ) -> None: self.yes_no = yes_no - self.up_down = up_down + self.swipe = swipe self.input = input + self.x = x + self.y = y + self.wait = wait @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('yes_no', p.BoolType, 0), - 2: ('up_down', p.BoolType, 0), - 3: ('input', p.UnicodeType, 0), + 1: ('yes_no', p.BoolType, None), + 2: ('swipe', p.EnumType("DebugSwipeDirection", (0, 1, 2, 3)), None), + 3: ('input', p.UnicodeType, None), + 4: ('x', p.UVarintType, None), + 5: ('y', p.UVarintType, None), + 6: ('wait', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkEraseSdCard.py b/hwilib/devices/trezorlib/messages/DebugLinkEraseSdCard.py new file mode 100644 index 000000000..37aa32f1c --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkEraseSdCard.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkEraseSdCard(p.MessageType): + MESSAGE_WIRE_TYPE = 9005 + + def __init__( + self, + *, + format: bool = None, + ) -> None: + self.format = format + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('format', p.BoolType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkFlashErase.py b/hwilib/devices/trezorlib/messages/DebugLinkFlashErase.py index 36507b7fa..4521db54f 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkFlashErase.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkFlashErase.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkFlashErase(p.MessageType): MESSAGE_WIRE_TYPE = 113 def __init__( self, + *, sector: int = None, ) -> None: self.sector = sector @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('sector', p.UVarintType, 0), + 1: ('sector', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkGetState.py b/hwilib/devices/trezorlib/messages/DebugLinkGetState.py index 7a089359d..4f5ef23c7 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkGetState.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkGetState.py @@ -2,6 +2,32 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkGetState(p.MessageType): MESSAGE_WIRE_TYPE = 101 + + def __init__( + self, + *, + wait_word_list: bool = None, + wait_word_pos: bool = None, + wait_layout: bool = None, + ) -> None: + self.wait_word_list = wait_word_list + self.wait_word_pos = wait_word_pos + self.wait_layout = wait_layout + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('wait_word_list', p.BoolType, None), + 2: ('wait_word_pos', p.BoolType, None), + 3: ('wait_layout', p.BoolType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkLog.py b/hwilib/devices/trezorlib/messages/DebugLinkLog.py deleted file mode 100644 index b22782986..000000000 --- a/hwilib/devices/trezorlib/messages/DebugLinkLog.py +++ /dev/null @@ -1,25 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -from .. import protobuf as p - - -class DebugLinkLog(p.MessageType): - MESSAGE_WIRE_TYPE = 104 - - def __init__( - self, - level: int = None, - bucket: str = None, - text: str = None, - ) -> None: - self.level = level - self.bucket = bucket - self.text = text - - @classmethod - def get_fields(cls): - return { - 1: ('level', p.UVarintType, 0), - 2: ('bucket', p.UnicodeType, 0), - 3: ('text', p.UnicodeType, 0), - } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkMemory.py b/hwilib/devices/trezorlib/messages/DebugLinkMemory.py index 0b5f4c5af..8a8a4291a 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkMemory.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkMemory.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkMemory(p.MessageType): MESSAGE_WIRE_TYPE = 111 def __init__( self, + *, memory: bytes = None, ) -> None: self.memory = memory @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('memory', p.BytesType, 0), + 1: ('memory', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkMemoryRead.py b/hwilib/devices/trezorlib/messages/DebugLinkMemoryRead.py index 36560c40a..e9e261c0a 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkMemoryRead.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkMemoryRead.py @@ -2,12 +2,20 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkMemoryRead(p.MessageType): MESSAGE_WIRE_TYPE = 110 def __init__( self, + *, address: int = None, length: int = None, ) -> None: @@ -15,8 +23,8 @@ def __init__( self.length = length @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UVarintType, 0), - 2: ('length', p.UVarintType, 0), + 1: ('address', p.UVarintType, None), + 2: ('length', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkMemoryWrite.py b/hwilib/devices/trezorlib/messages/DebugLinkMemoryWrite.py index d939f533a..2563607d8 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkMemoryWrite.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkMemoryWrite.py @@ -2,12 +2,20 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkMemoryWrite(p.MessageType): MESSAGE_WIRE_TYPE = 112 def __init__( self, + *, address: int = None, memory: bytes = None, flash: bool = None, @@ -17,9 +25,9 @@ def __init__( self.flash = flash @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UVarintType, 0), - 2: ('memory', p.BytesType, 0), - 3: ('flash', p.BoolType, 0), + 1: ('address', p.UVarintType, None), + 2: ('memory', p.BytesType, None), + 3: ('flash', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkRecordScreen.py b/hwilib/devices/trezorlib/messages/DebugLinkRecordScreen.py new file mode 100644 index 000000000..e18d08490 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkRecordScreen.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkRecordScreen(p.MessageType): + MESSAGE_WIRE_TYPE = 9003 + + def __init__( + self, + *, + target_directory: str = None, + ) -> None: + self.target_directory = target_directory + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('target_directory', p.UnicodeType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkReseedRandom.py b/hwilib/devices/trezorlib/messages/DebugLinkReseedRandom.py new file mode 100644 index 000000000..65b7ce028 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkReseedRandom.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkReseedRandom(p.MessageType): + MESSAGE_WIRE_TYPE = 9002 + + def __init__( + self, + *, + value: int = None, + ) -> None: + self.value = value + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('value', p.UVarintType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkShowText.py b/hwilib/devices/trezorlib/messages/DebugLinkShowText.py new file mode 100644 index 000000000..e6d3b79f6 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkShowText.py @@ -0,0 +1,38 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .DebugLinkShowTextItem import DebugLinkShowTextItem + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkShowText(p.MessageType): + MESSAGE_WIRE_TYPE = 9004 + + def __init__( + self, + *, + body_text: List[DebugLinkShowTextItem] = None, + header_text: str = None, + header_icon: str = None, + icon_color: str = None, + ) -> None: + self.body_text = body_text if body_text is not None else [] + self.header_text = header_text + self.header_icon = header_icon + self.icon_color = icon_color + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('header_text', p.UnicodeType, None), + 2: ('body_text', DebugLinkShowTextItem, p.FLAG_REPEATED), + 3: ('header_icon', p.UnicodeType, None), + 4: ('icon_color', p.UnicodeType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkShowTextItem.py b/hwilib/devices/trezorlib/messages/DebugLinkShowTextItem.py new file mode 100644 index 000000000..a8af170df --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkShowTextItem.py @@ -0,0 +1,30 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeDebugLinkShowTextStyle = Literal[0, 1, 2, 4, 5, 6] + except ImportError: + pass + + +class DebugLinkShowTextItem(p.MessageType): + + def __init__( + self, + *, + style: EnumTypeDebugLinkShowTextStyle = None, + content: str = None, + ) -> None: + self.style = style + self.content = content + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('style', p.EnumType("DebugLinkShowTextStyle", (0, 1, 2, 4, 5, 6)), None), + 2: ('content', p.UnicodeType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkShowTextStyle.py b/hwilib/devices/trezorlib/messages/DebugLinkShowTextStyle.py new file mode 100644 index 000000000..8ead39e99 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkShowTextStyle.py @@ -0,0 +1,14 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +NORMAL: Literal[0] = 0 +BOLD: Literal[1] = 1 +MONO: Literal[2] = 2 +BR: Literal[4] = 4 +BR_HALF: Literal[5] = 5 +SET_COLOR: Literal[6] = 6 diff --git a/hwilib/devices/trezorlib/messages/DebugLinkState.py b/hwilib/devices/trezorlib/messages/DebugLinkState.py index 5729102a2..8a8fbf731 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkState.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkState.py @@ -4,48 +4,62 @@ from .HDNodeType import HDNodeType +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkState(p.MessageType): MESSAGE_WIRE_TYPE = 102 def __init__( self, + *, + layout_lines: List[str] = None, layout: bytes = None, pin: str = None, matrix: str = None, - mnemonic: str = None, + mnemonic_secret: bytes = None, node: HDNodeType = None, passphrase_protection: bool = None, reset_word: str = None, reset_entropy: bytes = None, recovery_fake_word: str = None, recovery_word_pos: int = None, - # reset_word_pos: int = None, + reset_word_pos: int = None, + mnemonic_type: int = None, ) -> None: + self.layout_lines = layout_lines if layout_lines is not None else [] self.layout = layout self.pin = pin self.matrix = matrix - self.mnemonic = mnemonic + self.mnemonic_secret = mnemonic_secret self.node = node self.passphrase_protection = passphrase_protection self.reset_word = reset_word self.reset_entropy = reset_entropy self.recovery_fake_word = recovery_fake_word self.recovery_word_pos = recovery_word_pos - # self.reset_word_pos = reset_word_pos + self.reset_word_pos = reset_word_pos + self.mnemonic_type = mnemonic_type @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('layout', p.BytesType, 0), - 2: ('pin', p.UnicodeType, 0), - 3: ('matrix', p.UnicodeType, 0), - 4: ('mnemonic', p.UnicodeType, 0), - 5: ('node', HDNodeType, 0), - 6: ('passphrase_protection', p.BoolType, 0), - 7: ('reset_word', p.UnicodeType, 0), - 8: ('reset_entropy', p.BytesType, 0), - 9: ('recovery_fake_word', p.UnicodeType, 0), - 10: ('recovery_word_pos', p.UVarintType, 0), - # 11: ('reset_word_pos', p.UVarintType, 0), + 1: ('layout', p.BytesType, None), + 2: ('pin', p.UnicodeType, None), + 3: ('matrix', p.UnicodeType, None), + 4: ('mnemonic_secret', p.BytesType, None), + 5: ('node', HDNodeType, None), + 6: ('passphrase_protection', p.BoolType, None), + 7: ('reset_word', p.UnicodeType, None), + 8: ('reset_entropy', p.BytesType, None), + 9: ('recovery_fake_word', p.UnicodeType, None), + 10: ('recovery_word_pos', p.UVarintType, None), + 11: ('reset_word_pos', p.UVarintType, None), + 12: ('mnemonic_type', p.UVarintType, None), + 13: ('layout_lines', p.UnicodeType, p.FLAG_REPEATED), } diff --git a/hwilib/devices/trezorlib/messages/DebugLinkStop.py b/hwilib/devices/trezorlib/messages/DebugLinkStop.py index 3a63a1020..3910038e1 100644 --- a/hwilib/devices/trezorlib/messages/DebugLinkStop.py +++ b/hwilib/devices/trezorlib/messages/DebugLinkStop.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class DebugLinkStop(p.MessageType): MESSAGE_WIRE_TYPE = 103 diff --git a/hwilib/devices/trezorlib/messages/DebugLinkWatchLayout.py b/hwilib/devices/trezorlib/messages/DebugLinkWatchLayout.py new file mode 100644 index 000000000..e6215ad0c --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugLinkWatchLayout.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DebugLinkWatchLayout(p.MessageType): + MESSAGE_WIRE_TYPE = 9006 + + def __init__( + self, + *, + watch: bool = None, + ) -> None: + self.watch = watch + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('watch', p.BoolType, None), + } diff --git a/hwilib/devices/trezorlib/messages/DebugSwipeDirection.py b/hwilib/devices/trezorlib/messages/DebugSwipeDirection.py new file mode 100644 index 000000000..9e6cbf292 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DebugSwipeDirection.py @@ -0,0 +1,12 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +UP: Literal[0] = 0 +DOWN: Literal[1] = 1 +LEFT: Literal[2] = 2 +RIGHT: Literal[3] = 3 diff --git a/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py index 475b9d2c6..fa178ddf5 100644 --- a/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py +++ b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateAck.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Deprecated_PassphraseStateAck(p.MessageType): MESSAGE_WIRE_TYPE = 78 diff --git a/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py index aa9a54681..579957ff0 100644 --- a/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py +++ b/hwilib/devices/trezorlib/messages/Deprecated_PassphraseStateRequest.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Deprecated_PassphraseStateRequest(p.MessageType): MESSAGE_WIRE_TYPE = 77 def __init__( self, - _state: bytes = None, + *, + state: bytes = None, ) -> None: - self._state = _state + self.state = state @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('_state', p.BytesType, 0), + 1: ('state', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/DoPreauthorized.py b/hwilib/devices/trezorlib/messages/DoPreauthorized.py new file mode 100644 index 000000000..7add79a39 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/DoPreauthorized.py @@ -0,0 +1,14 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class DoPreauthorized(p.MessageType): + MESSAGE_WIRE_TYPE = 84 diff --git a/hwilib/devices/trezorlib/messages/EndSession.py b/hwilib/devices/trezorlib/messages/EndSession.py new file mode 100644 index 000000000..667213f68 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/EndSession.py @@ -0,0 +1,14 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class EndSession(p.MessageType): + MESSAGE_WIRE_TYPE = 83 diff --git a/hwilib/devices/trezorlib/messages/Entropy.py b/hwilib/devices/trezorlib/messages/Entropy.py index e11f389bf..8c8c53cbf 100644 --- a/hwilib/devices/trezorlib/messages/Entropy.py +++ b/hwilib/devices/trezorlib/messages/Entropy.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Entropy(p.MessageType): MESSAGE_WIRE_TYPE = 10 def __init__( self, - entropy: bytes = None, + *, + entropy: bytes, ) -> None: self.entropy = entropy @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('entropy', p.BytesType, 0), # required + 1: ('entropy', p.BytesType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/EntropyAck.py b/hwilib/devices/trezorlib/messages/EntropyAck.py index c6a08eaaa..0e5ac296c 100644 --- a/hwilib/devices/trezorlib/messages/EntropyAck.py +++ b/hwilib/devices/trezorlib/messages/EntropyAck.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class EntropyAck(p.MessageType): MESSAGE_WIRE_TYPE = 36 def __init__( self, + *, entropy: bytes = None, ) -> None: self.entropy = entropy @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('entropy', p.BytesType, 0), + 1: ('entropy', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/EntropyRequest.py b/hwilib/devices/trezorlib/messages/EntropyRequest.py index 418668670..c74735448 100644 --- a/hwilib/devices/trezorlib/messages/EntropyRequest.py +++ b/hwilib/devices/trezorlib/messages/EntropyRequest.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class EntropyRequest(p.MessageType): MESSAGE_WIRE_TYPE = 35 diff --git a/hwilib/devices/trezorlib/messages/Failure.py b/hwilib/devices/trezorlib/messages/Failure.py index 2753763c7..d26cc3cb1 100644 --- a/hwilib/devices/trezorlib/messages/Failure.py +++ b/hwilib/devices/trezorlib/messages/Failure.py @@ -2,21 +2,30 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeFailureType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 99] + except ImportError: + pass + class Failure(p.MessageType): MESSAGE_WIRE_TYPE = 3 def __init__( self, - code: int = None, + *, + code: EnumTypeFailureType = None, message: str = None, ) -> None: self.code = code self.message = message @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('code', p.UVarintType, 0), - 2: ('message', p.UnicodeType, 0), + 1: ('code', p.EnumType("FailureType", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 99)), None), + 2: ('message', p.UnicodeType, None), } diff --git a/hwilib/devices/trezorlib/messages/FailureType.py b/hwilib/devices/trezorlib/messages/FailureType.py index 6f5cffe18..49dbf8b6f 100644 --- a/hwilib/devices/trezorlib/messages/FailureType.py +++ b/hwilib/devices/trezorlib/messages/FailureType.py @@ -1,15 +1,23 @@ # Automatically generated by pb2py # fmt: off -UnexpectedMessage = 1 -ButtonExpected = 2 -DataError = 3 -ActionCancelled = 4 -PinExpected = 5 -PinCancelled = 6 -PinInvalid = 7 -InvalidSignature = 8 -ProcessError = 9 -NotEnoughFunds = 10 -NotInitialized = 11 -PinMismatch = 12 -FirmwareError = 99 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +UnexpectedMessage: Literal[1] = 1 +ButtonExpected: Literal[2] = 2 +DataError: Literal[3] = 3 +ActionCancelled: Literal[4] = 4 +PinExpected: Literal[5] = 5 +PinCancelled: Literal[6] = 6 +PinInvalid: Literal[7] = 7 +InvalidSignature: Literal[8] = 8 +ProcessError: Literal[9] = 9 +NotEnoughFunds: Literal[10] = 10 +NotInitialized: Literal[11] = 11 +PinMismatch: Literal[12] = 12 +WipeCodeMismatch: Literal[13] = 13 +InvalidSession: Literal[14] = 14 +FirmwareError: Literal[99] = 99 diff --git a/hwilib/devices/trezorlib/messages/Features.py b/hwilib/devices/trezorlib/messages/Features.py index 93a681d0a..1e8d08085 100644 --- a/hwilib/devices/trezorlib/messages/Features.py +++ b/hwilib/devices/trezorlib/messages/Features.py @@ -2,12 +2,24 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeCapability = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + EnumTypeBackupType = Literal[0, 1, 2] + EnumTypeSafetyCheckLevel = Literal[0, 1, 2] + except ImportError: + pass + class Features(p.MessageType): MESSAGE_WIRE_TYPE = 17 def __init__( self, + *, + capabilities: List[EnumTypeCapability] = None, vendor: str = None, major_version: int = None, minor_version: int = None, @@ -22,8 +34,7 @@ def __init__( revision: bytes = None, bootloader_hash: bytes = None, imported: bool = None, - pin_cached: bool = None, - passphrase_cached: bool = None, + unlocked: bool = None, firmware_present: bool = None, needs_backup: bool = None, flags: int = None, @@ -35,9 +46,19 @@ def __init__( fw_vendor_keys: bytes = None, unfinished_backup: bool = None, no_backup: bool = None, + recovery_mode: bool = None, + backup_type: EnumTypeBackupType = None, + sd_card_present: bool = None, + sd_protection: bool = None, + wipe_code_protection: bool = None, session_id: bytes = None, passphrase_always_on_device: bool = None, + safety_checks: EnumTypeSafetyCheckLevel = None, + auto_lock_delay_ms: int = None, + display_rotation: int = None, + experimental_features: bool = None, ) -> None: + self.capabilities = capabilities if capabilities is not None else [] self.vendor = vendor self.major_version = major_version self.minor_version = minor_version @@ -52,8 +73,7 @@ def __init__( self.revision = revision self.bootloader_hash = bootloader_hash self.imported = imported - self.pin_cached = pin_cached - self.passphrase_cached = passphrase_cached + self.unlocked = unlocked self.firmware_present = firmware_present self.needs_backup = needs_backup self.flags = flags @@ -65,39 +85,57 @@ def __init__( self.fw_vendor_keys = fw_vendor_keys self.unfinished_backup = unfinished_backup self.no_backup = no_backup + self.recovery_mode = recovery_mode + self.backup_type = backup_type + self.sd_card_present = sd_card_present + self.sd_protection = sd_protection + self.wipe_code_protection = wipe_code_protection self.session_id = session_id self.passphrase_always_on_device = passphrase_always_on_device + self.safety_checks = safety_checks + self.auto_lock_delay_ms = auto_lock_delay_ms + self.display_rotation = display_rotation + self.experimental_features = experimental_features @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('vendor', p.UnicodeType, 0), - 2: ('major_version', p.UVarintType, 0), - 3: ('minor_version', p.UVarintType, 0), - 4: ('patch_version', p.UVarintType, 0), - 5: ('bootloader_mode', p.BoolType, 0), - 6: ('device_id', p.UnicodeType, 0), - 7: ('pin_protection', p.BoolType, 0), - 8: ('passphrase_protection', p.BoolType, 0), - 9: ('language', p.UnicodeType, 0), - 10: ('label', p.UnicodeType, 0), - 12: ('initialized', p.BoolType, 0), - 13: ('revision', p.BytesType, 0), - 14: ('bootloader_hash', p.BytesType, 0), - 15: ('imported', p.BoolType, 0), - 16: ('pin_cached', p.BoolType, 0), - 17: ('passphrase_cached', p.BoolType, 0), - # 18: ('firmware_present', p.BoolType, 0), - # 19: ('needs_backup', p.BoolType, 0), - # 20: ('flags', p.UVarintType, 0), - 21: ('model', p.UnicodeType, 0), - # 22: ('fw_major', p.UVarintType, 0), - # 23: ('fw_minor', p.UVarintType, 0), - # 24: ('fw_patch', p.UVarintType, 0), - # 25: ('fw_vendor', p.UnicodeType, 0), - # 26: ('fw_vendor_keys', p.BytesType, 0), - # 27: ('unfinished_backup', p.BoolType, 0), - # 28: ('no_backup', p.BoolType, 0), - 35: ('session_id', p.BytesType, 0), - 36: ('passphrase_always_on_device', p.BoolType, 0), + 1: ('vendor', p.UnicodeType, None), + 2: ('major_version', p.UVarintType, None), + 3: ('minor_version', p.UVarintType, None), + 4: ('patch_version', p.UVarintType, None), + 5: ('bootloader_mode', p.BoolType, None), + 6: ('device_id', p.UnicodeType, None), + 7: ('pin_protection', p.BoolType, None), + 8: ('passphrase_protection', p.BoolType, None), + 9: ('language', p.UnicodeType, None), + 10: ('label', p.UnicodeType, None), + 12: ('initialized', p.BoolType, None), + 13: ('revision', p.BytesType, None), + 14: ('bootloader_hash', p.BytesType, None), + 15: ('imported', p.BoolType, None), + 16: ('unlocked', p.BoolType, None), + 18: ('firmware_present', p.BoolType, None), + 19: ('needs_backup', p.BoolType, None), + 20: ('flags', p.UVarintType, None), + 21: ('model', p.UnicodeType, None), + 22: ('fw_major', p.UVarintType, None), + 23: ('fw_minor', p.UVarintType, None), + 24: ('fw_patch', p.UVarintType, None), + 25: ('fw_vendor', p.UnicodeType, None), + 26: ('fw_vendor_keys', p.BytesType, None), + 27: ('unfinished_backup', p.BoolType, None), + 28: ('no_backup', p.BoolType, None), + 29: ('recovery_mode', p.BoolType, None), + 30: ('capabilities', p.EnumType("Capability", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)), p.FLAG_REPEATED), + 31: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), None), + 32: ('sd_card_present', p.BoolType, None), + 33: ('sd_protection', p.BoolType, None), + 34: ('wipe_code_protection', p.BoolType, None), + 35: ('session_id', p.BytesType, None), + 36: ('passphrase_always_on_device', p.BoolType, None), + 37: ('safety_checks', p.EnumType("SafetyCheckLevel", (0, 1, 2)), None), + 38: ('auto_lock_delay_ms', p.UVarintType, None), + 39: ('display_rotation', p.UVarintType, None), + 40: ('experimental_features', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/FirmwareErase.py b/hwilib/devices/trezorlib/messages/FirmwareErase.py index c07c2f910..f5a4e8d33 100644 --- a/hwilib/devices/trezorlib/messages/FirmwareErase.py +++ b/hwilib/devices/trezorlib/messages/FirmwareErase.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class FirmwareErase(p.MessageType): MESSAGE_WIRE_TYPE = 6 def __init__( self, + *, length: int = None, ) -> None: self.length = length @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('length', p.UVarintType, 0), + 1: ('length', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/FirmwareRequest.py b/hwilib/devices/trezorlib/messages/FirmwareRequest.py index 13ba2ce1b..649019333 100644 --- a/hwilib/devices/trezorlib/messages/FirmwareRequest.py +++ b/hwilib/devices/trezorlib/messages/FirmwareRequest.py @@ -2,12 +2,20 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class FirmwareRequest(p.MessageType): MESSAGE_WIRE_TYPE = 8 def __init__( self, + *, offset: int = None, length: int = None, ) -> None: @@ -15,8 +23,8 @@ def __init__( self.length = length @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('offset', p.UVarintType, 0), - 2: ('length', p.UVarintType, 0), + 1: ('offset', p.UVarintType, None), + 2: ('length', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/FirmwareUpload.py b/hwilib/devices/trezorlib/messages/FirmwareUpload.py index 217273ed6..34a406e36 100644 --- a/hwilib/devices/trezorlib/messages/FirmwareUpload.py +++ b/hwilib/devices/trezorlib/messages/FirmwareUpload.py @@ -2,21 +2,29 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class FirmwareUpload(p.MessageType): MESSAGE_WIRE_TYPE = 7 def __init__( self, - payload: bytes = None, + *, + payload: bytes, hash: bytes = None, ) -> None: self.payload = payload self.hash = hash @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('payload', p.BytesType, 0), # required - 2: ('hash', p.BytesType, 0), + 1: ('payload', p.BytesType, p.FLAG_REQUIRED), + 2: ('hash', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/GetAddress.py b/hwilib/devices/trezorlib/messages/GetAddress.py index ce876512a..03d6cc531 100644 --- a/hwilib/devices/trezorlib/messages/GetAddress.py +++ b/hwilib/devices/trezorlib/messages/GetAddress.py @@ -6,9 +6,11 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] except ImportError: - List = None # type: ignore + pass class GetAddress(p.MessageType): @@ -16,24 +18,28 @@ class GetAddress(p.MessageType): def __init__( self, + *, address_n: List[int] = None, - coin_name: str = None, + coin_name: str = "Bitcoin", show_display: bool = None, multisig: MultisigRedeemScriptType = None, - script_type: int = None, + script_type: EnumTypeInputScriptType = 0, + ignore_xpub_magic: bool = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.coin_name = coin_name self.show_display = show_display self.multisig = multisig self.script_type = script_type + self.ignore_xpub_magic = ignore_xpub_magic @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), - 2: ('coin_name', p.UnicodeType, 0), # default=Bitcoin - 3: ('show_display', p.BoolType, 0), - 4: ('multisig', MultisigRedeemScriptType, 0), - 5: ('script_type', p.UVarintType, 0), # default=SPENDADDRESS + 2: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 3: ('show_display', p.BoolType, None), + 4: ('multisig', MultisigRedeemScriptType, None), + 5: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS + 6: ('ignore_xpub_magic', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/GetEntropy.py b/hwilib/devices/trezorlib/messages/GetEntropy.py index 0a606c7d8..bc0628fe5 100644 --- a/hwilib/devices/trezorlib/messages/GetEntropy.py +++ b/hwilib/devices/trezorlib/messages/GetEntropy.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class GetEntropy(p.MessageType): MESSAGE_WIRE_TYPE = 9 def __init__( self, - size: int = None, + *, + size: int, ) -> None: self.size = size @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('size', p.UVarintType, 0), # required + 1: ('size', p.UVarintType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/GetFeatures.py b/hwilib/devices/trezorlib/messages/GetFeatures.py index bdbcab35b..5b6a9dc06 100644 --- a/hwilib/devices/trezorlib/messages/GetFeatures.py +++ b/hwilib/devices/trezorlib/messages/GetFeatures.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class GetFeatures(p.MessageType): MESSAGE_WIRE_TYPE = 55 diff --git a/hwilib/devices/trezorlib/messages/GetOwnershipId.py b/hwilib/devices/trezorlib/messages/GetOwnershipId.py new file mode 100644 index 000000000..548ad5ad9 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/GetOwnershipId.py @@ -0,0 +1,39 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .MultisigRedeemScriptType import MultisigRedeemScriptType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + except ImportError: + pass + + +class GetOwnershipId(p.MessageType): + MESSAGE_WIRE_TYPE = 43 + + def __init__( + self, + *, + address_n: List[int] = None, + coin_name: str = "Bitcoin", + multisig: MultisigRedeemScriptType = None, + script_type: EnumTypeInputScriptType = 0, + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.coin_name = coin_name + self.multisig = multisig + self.script_type = script_type + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 2: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 3: ('multisig', MultisigRedeemScriptType, None), + 4: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS + } diff --git a/hwilib/devices/trezorlib/messages/GetOwnershipProof.py b/hwilib/devices/trezorlib/messages/GetOwnershipProof.py new file mode 100644 index 000000000..579373ac0 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/GetOwnershipProof.py @@ -0,0 +1,48 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +from .MultisigRedeemScriptType import MultisigRedeemScriptType + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + except ImportError: + pass + + +class GetOwnershipProof(p.MessageType): + MESSAGE_WIRE_TYPE = 49 + + def __init__( + self, + *, + address_n: List[int] = None, + ownership_ids: List[bytes] = None, + coin_name: str = "Bitcoin", + script_type: EnumTypeInputScriptType = 3, + multisig: MultisigRedeemScriptType = None, + user_confirmation: bool = False, + commitment_data: bytes = b"", + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.ownership_ids = ownership_ids if ownership_ids is not None else [] + self.coin_name = coin_name + self.script_type = script_type + self.multisig = multisig + self.user_confirmation = user_confirmation + self.commitment_data = commitment_data + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 2: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 3: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 3), # default=SPENDWITNESS + 4: ('multisig', MultisigRedeemScriptType, None), + 5: ('user_confirmation', p.BoolType, False), # default=false + 6: ('ownership_ids', p.BytesType, p.FLAG_REPEATED), + 7: ('commitment_data', p.BytesType, b""), # default= + } diff --git a/hwilib/devices/trezorlib/messages/GetPublicKey.py b/hwilib/devices/trezorlib/messages/GetPublicKey.py index f488eb662..0c358237d 100644 --- a/hwilib/devices/trezorlib/messages/GetPublicKey.py +++ b/hwilib/devices/trezorlib/messages/GetPublicKey.py @@ -4,9 +4,11 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] except ImportError: - List = None # type: ignore + pass class GetPublicKey(p.MessageType): @@ -14,24 +16,28 @@ class GetPublicKey(p.MessageType): def __init__( self, + *, address_n: List[int] = None, ecdsa_curve_name: str = None, show_display: bool = None, - coin_name: str = None, - script_type: int = None, + coin_name: str = "Bitcoin", + script_type: EnumTypeInputScriptType = 0, + ignore_xpub_magic: bool = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.ecdsa_curve_name = ecdsa_curve_name self.show_display = show_display self.coin_name = coin_name self.script_type = script_type + self.ignore_xpub_magic = ignore_xpub_magic @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), - 2: ('ecdsa_curve_name', p.UnicodeType, 0), - 3: ('show_display', p.BoolType, 0), - 4: ('coin_name', p.UnicodeType, 0), # default=Bitcoin - 5: ('script_type', p.UVarintType, 0), # default=SPENDADDRESS + 2: ('ecdsa_curve_name', p.UnicodeType, None), + 3: ('show_display', p.BoolType, None), + 4: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 5: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS + 6: ('ignore_xpub_magic', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/HDNodePathType.py b/hwilib/devices/trezorlib/messages/HDNodePathType.py index 3e275acb4..d0a6585a7 100644 --- a/hwilib/devices/trezorlib/messages/HDNodePathType.py +++ b/hwilib/devices/trezorlib/messages/HDNodePathType.py @@ -6,24 +6,26 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 except ImportError: - List = None # type: ignore + pass class HDNodePathType(p.MessageType): def __init__( self, - node: HDNodeType = None, + *, + node: HDNodeType, address_n: List[int] = None, ) -> None: - self.node = node self.address_n = address_n if address_n is not None else [] + self.node = node @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('node', HDNodeType, 0), # required + 1: ('node', HDNodeType, p.FLAG_REQUIRED), 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), } diff --git a/hwilib/devices/trezorlib/messages/HDNodeType.py b/hwilib/devices/trezorlib/messages/HDNodeType.py index 4577532b7..1a0c01f4b 100644 --- a/hwilib/devices/trezorlib/messages/HDNodeType.py +++ b/hwilib/devices/trezorlib/messages/HDNodeType.py @@ -2,32 +2,40 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class HDNodeType(p.MessageType): def __init__( self, - depth: int = None, - fingerprint: int = None, - child_num: int = None, - chain_code: bytes = None, + *, + depth: int, + fingerprint: int, + child_num: int, + chain_code: bytes, + public_key: bytes, private_key: bytes = None, - public_key: bytes = None, ) -> None: self.depth = depth self.fingerprint = fingerprint self.child_num = child_num self.chain_code = chain_code - self.private_key = private_key self.public_key = public_key + self.private_key = private_key @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('depth', p.UVarintType, 0), # required - 2: ('fingerprint', p.UVarintType, 0), # required - 3: ('child_num', p.UVarintType, 0), # required - 4: ('chain_code', p.BytesType, 0), # required - 5: ('private_key', p.BytesType, 0), - 6: ('public_key', p.BytesType, 0), + 1: ('depth', p.UVarintType, p.FLAG_REQUIRED), + 2: ('fingerprint', p.UVarintType, p.FLAG_REQUIRED), + 3: ('child_num', p.UVarintType, p.FLAG_REQUIRED), + 4: ('chain_code', p.BytesType, p.FLAG_REQUIRED), + 5: ('private_key', p.BytesType, None), + 6: ('public_key', p.BytesType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/IdentityType.py b/hwilib/devices/trezorlib/messages/IdentityType.py deleted file mode 100644 index 1e5125b84..000000000 --- a/hwilib/devices/trezorlib/messages/IdentityType.py +++ /dev/null @@ -1,33 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -from .. import protobuf as p - - -class IdentityType(p.MessageType): - - def __init__( - self, - proto: str = None, - user: str = None, - host: str = None, - port: str = None, - path: str = None, - index: int = None, - ) -> None: - self.proto = proto - self.user = user - self.host = host - self.port = port - self.path = path - self.index = index - - @classmethod - def get_fields(cls): - return { - 1: ('proto', p.UnicodeType, 0), - 2: ('user', p.UnicodeType, 0), - 3: ('host', p.UnicodeType, 0), - 4: ('port', p.UnicodeType, 0), - 5: ('path', p.UnicodeType, 0), - 6: ('index', p.UVarintType, 0), # default=0 - } diff --git a/hwilib/devices/trezorlib/messages/Initialize.py b/hwilib/devices/trezorlib/messages/Initialize.py index 01663d74a..7eb56aeb8 100644 --- a/hwilib/devices/trezorlib/messages/Initialize.py +++ b/hwilib/devices/trezorlib/messages/Initialize.py @@ -2,21 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Initialize(p.MessageType): MESSAGE_WIRE_TYPE = 0 def __init__( self, - state: bytes = None, - skip_passphrase: bool = None, + *, + session_id: bytes = None, ) -> None: - self.state = state - self.skip_passphrase = skip_passphrase + self.session_id = session_id @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('state', p.BytesType, 0), - 2: ('skip_passphrase', p.BoolType, 0), + 1: ('session_id', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/InputScriptType.py b/hwilib/devices/trezorlib/messages/InputScriptType.py index 09b8332cb..26243b1ca 100644 --- a/hwilib/devices/trezorlib/messages/InputScriptType.py +++ b/hwilib/devices/trezorlib/messages/InputScriptType.py @@ -1,7 +1,13 @@ # Automatically generated by pb2py # fmt: off -SPENDADDRESS = 0 -SPENDMULTISIG = 1 -EXTERNAL = 2 -SPENDWITNESS = 3 -SPENDP2SHWITNESS = 4 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +SPENDADDRESS: Literal[0] = 0 +SPENDMULTISIG: Literal[1] = 1 +EXTERNAL: Literal[2] = 2 +SPENDWITNESS: Literal[3] = 3 +SPENDP2SHWITNESS: Literal[4] = 4 diff --git a/hwilib/devices/trezorlib/messages/LoadDevice.py b/hwilib/devices/trezorlib/messages/LoadDevice.py index 534563cb0..748848e98 100644 --- a/hwilib/devices/trezorlib/messages/LoadDevice.py +++ b/hwilib/devices/trezorlib/messages/LoadDevice.py @@ -2,7 +2,12 @@ # fmt: off from .. import protobuf as p -from .HDNodeType import HDNodeType +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass class LoadDevice(p.MessageType): @@ -10,33 +15,37 @@ class LoadDevice(p.MessageType): def __init__( self, - mnemonic: str = None, - node: HDNodeType = None, + *, + mnemonics: List[str] = None, pin: str = None, passphrase_protection: bool = None, - language: str = None, + language: str = "en-US", label: str = None, skip_checksum: bool = None, u2f_counter: int = None, + needs_backup: bool = None, + no_backup: bool = None, ) -> None: - self.mnemonic = mnemonic - self.node = node + self.mnemonics = mnemonics if mnemonics is not None else [] self.pin = pin self.passphrase_protection = passphrase_protection self.language = language self.label = label self.skip_checksum = skip_checksum self.u2f_counter = u2f_counter + self.needs_backup = needs_backup + self.no_backup = no_backup @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('mnemonic', p.UnicodeType, 0), - 2: ('node', HDNodeType, 0), - 3: ('pin', p.UnicodeType, 0), - 4: ('passphrase_protection', p.BoolType, 0), - 5: ('language', p.UnicodeType, 0), # default=english - 6: ('label', p.UnicodeType, 0), - 7: ('skip_checksum', p.BoolType, 0), - 8: ('u2f_counter', p.UVarintType, 0), + 1: ('mnemonics', p.UnicodeType, p.FLAG_REPEATED), + 3: ('pin', p.UnicodeType, None), + 4: ('passphrase_protection', p.BoolType, None), + 5: ('language', p.UnicodeType, "en-US"), # default=en-US + 6: ('label', p.UnicodeType, None), + 7: ('skip_checksum', p.BoolType, None), + 8: ('u2f_counter', p.UVarintType, None), + 9: ('needs_backup', p.BoolType, None), + 10: ('no_backup', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/LockDevice.py b/hwilib/devices/trezorlib/messages/LockDevice.py new file mode 100644 index 000000000..cc1226223 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/LockDevice.py @@ -0,0 +1,14 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class LockDevice(p.MessageType): + MESSAGE_WIRE_TYPE = 24 diff --git a/hwilib/devices/trezorlib/messages/MessageSignature.py b/hwilib/devices/trezorlib/messages/MessageSignature.py index 70aa8416b..c995a011d 100644 --- a/hwilib/devices/trezorlib/messages/MessageSignature.py +++ b/hwilib/devices/trezorlib/messages/MessageSignature.py @@ -2,12 +2,20 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class MessageSignature(p.MessageType): MESSAGE_WIRE_TYPE = 40 def __init__( self, + *, address: str = None, signature: bytes = None, ) -> None: @@ -15,8 +23,8 @@ def __init__( self.signature = signature @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UnicodeType, 0), - 2: ('signature', p.BytesType, 0), + 1: ('address', p.UnicodeType, None), + 2: ('signature', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/MessageType.py b/hwilib/devices/trezorlib/messages/MessageType.py index d6ebc1743..09f04aaa6 100644 --- a/hwilib/devices/trezorlib/messages/MessageType.py +++ b/hwilib/devices/trezorlib/messages/MessageType.py @@ -1,57 +1,73 @@ # Automatically generated by pb2py # fmt: off -Initialize = 0 -Ping = 1 -Success = 2 -Failure = 3 -ChangePin = 4 -WipeDevice = 5 -GetEntropy = 9 -Entropy = 10 -LoadDevice = 13 -ResetDevice = 14 -Features = 17 -PinMatrixRequest = 18 -PinMatrixAck = 19 -Cancel = 20 -ClearSession = 24 -ApplySettings = 25 -ButtonRequest = 26 -ButtonAck = 27 -ApplyFlags = 28 -BackupDevice = 34 -EntropyRequest = 35 -EntropyAck = 36 -PassphraseRequest = 41 -PassphraseAck = 42 -Deprecated_PassphraseStateRequest = 77 -Deprecated_PassphraseStateAck = 78 -RecoveryDevice = 45 -WordRequest = 46 -WordAck = 47 -GetFeatures = 55 -FirmwareErase = 6 -FirmwareUpload = 7 -FirmwareRequest = 8 -SelfTest = 32 -GetPublicKey = 11 -PublicKey = 12 -SignTx = 15 -TxRequest = 21 -TxAck = 22 -GetAddress = 29 -Address = 30 -SignMessage = 38 -VerifyMessage = 39 -MessageSignature = 40 -SignIdentity = 53 -SignedIdentity = 54 -DebugLinkDecision = 100 -DebugLinkGetState = 101 -DebugLinkState = 102 -DebugLinkStop = 103 -DebugLinkLog = 104 -DebugLinkMemoryRead = 110 -DebugLinkMemory = 111 -DebugLinkMemoryWrite = 112 -DebugLinkFlashErase = 113 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Initialize: Literal[0] = 0 +Ping: Literal[1] = 1 +Success: Literal[2] = 2 +Failure: Literal[3] = 3 +ChangePin: Literal[4] = 4 +WipeDevice: Literal[5] = 5 +GetEntropy: Literal[9] = 9 +Entropy: Literal[10] = 10 +LoadDevice: Literal[13] = 13 +ResetDevice: Literal[14] = 14 +Features: Literal[17] = 17 +PinMatrixRequest: Literal[18] = 18 +PinMatrixAck: Literal[19] = 19 +Cancel: Literal[20] = 20 +LockDevice: Literal[24] = 24 +ApplySettings: Literal[25] = 25 +ButtonRequest: Literal[26] = 26 +ButtonAck: Literal[27] = 27 +ApplyFlags: Literal[28] = 28 +BackupDevice: Literal[34] = 34 +EntropyRequest: Literal[35] = 35 +EntropyAck: Literal[36] = 36 +PassphraseRequest: Literal[41] = 41 +PassphraseAck: Literal[42] = 42 +RecoveryDevice: Literal[45] = 45 +WordRequest: Literal[46] = 46 +WordAck: Literal[47] = 47 +GetFeatures: Literal[55] = 55 +EndSession: Literal[83] = 83 +DoPreauthorized: Literal[84] = 84 +PreauthorizedRequest: Literal[85] = 85 +Deprecated_PassphraseStateRequest: Literal[77] = 77 +Deprecated_PassphraseStateAck: Literal[78] = 78 +FirmwareErase: Literal[6] = 6 +FirmwareUpload: Literal[7] = 7 +FirmwareRequest: Literal[8] = 8 +SelfTest: Literal[32] = 32 +GetPublicKey: Literal[11] = 11 +PublicKey: Literal[12] = 12 +SignTx: Literal[15] = 15 +TxRequest: Literal[21] = 21 +TxAck: Literal[22] = 22 +GetAddress: Literal[29] = 29 +Address: Literal[30] = 30 +SignMessage: Literal[38] = 38 +VerifyMessage: Literal[39] = 39 +MessageSignature: Literal[40] = 40 +GetOwnershipId: Literal[43] = 43 +OwnershipId: Literal[44] = 44 +GetOwnershipProof: Literal[49] = 49 +OwnershipProof: Literal[50] = 50 +AuthorizeCoinJoin: Literal[51] = 51 +DebugLinkDecision: Literal[100] = 100 +DebugLinkGetState: Literal[101] = 101 +DebugLinkState: Literal[102] = 102 +DebugLinkStop: Literal[103] = 103 +DebugLinkMemoryRead: Literal[110] = 110 +DebugLinkMemory: Literal[111] = 111 +DebugLinkMemoryWrite: Literal[112] = 112 +DebugLinkFlashErase: Literal[113] = 113 +DebugLinkReseedRandom: Literal[9002] = 9002 +DebugLinkRecordScreen: Literal[9003] = 9003 +DebugLinkShowText: Literal[9004] = 9004 +DebugLinkEraseSdCard: Literal[9005] = 9005 +DebugLinkWatchLayout: Literal[9006] = 9006 diff --git a/hwilib/devices/trezorlib/messages/MultisigRedeemScriptType.py b/hwilib/devices/trezorlib/messages/MultisigRedeemScriptType.py index 705cfe16c..614e45b69 100644 --- a/hwilib/devices/trezorlib/messages/MultisigRedeemScriptType.py +++ b/hwilib/devices/trezorlib/messages/MultisigRedeemScriptType.py @@ -3,30 +3,39 @@ from .. import protobuf as p from .HDNodePathType import HDNodePathType +from .HDNodeType import HDNodeType if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 except ImportError: - List = None # type: ignore + pass class MultisigRedeemScriptType(p.MessageType): def __init__( self, + *, + m: int, pubkeys: List[HDNodePathType] = None, signatures: List[bytes] = None, - m: int = None, + nodes: List[HDNodeType] = None, + address_n: List[int] = None, ) -> None: self.pubkeys = pubkeys if pubkeys is not None else [] self.signatures = signatures if signatures is not None else [] + self.nodes = nodes if nodes is not None else [] + self.address_n = address_n if address_n is not None else [] self.m = m @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { 1: ('pubkeys', HDNodePathType, p.FLAG_REPEATED), 2: ('signatures', p.BytesType, p.FLAG_REPEATED), - 3: ('m', p.UVarintType, 0), + 3: ('m', p.UVarintType, p.FLAG_REQUIRED), + 4: ('nodes', HDNodeType, p.FLAG_REPEATED), + 5: ('address_n', p.UVarintType, p.FLAG_REPEATED), } diff --git a/hwilib/devices/trezorlib/messages/OutputScriptType.py b/hwilib/devices/trezorlib/messages/OutputScriptType.py index 6a9b7eb39..94116b236 100644 --- a/hwilib/devices/trezorlib/messages/OutputScriptType.py +++ b/hwilib/devices/trezorlib/messages/OutputScriptType.py @@ -1,8 +1,14 @@ # Automatically generated by pb2py # fmt: off -PAYTOADDRESS = 0 -PAYTOSCRIPTHASH = 1 -PAYTOMULTISIG = 2 -PAYTOOPRETURN = 3 -PAYTOWITNESS = 4 -PAYTOP2SHWITNESS = 5 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +PAYTOADDRESS: Literal[0] = 0 +PAYTOSCRIPTHASH: Literal[1] = 1 +PAYTOMULTISIG: Literal[2] = 2 +PAYTOOPRETURN: Literal[3] = 3 +PAYTOWITNESS: Literal[4] = 4 +PAYTOP2SHWITNESS: Literal[5] = 5 diff --git a/hwilib/devices/trezorlib/messages/OwnershipId.py b/hwilib/devices/trezorlib/messages/OwnershipId.py new file mode 100644 index 000000000..dcffb16b8 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/OwnershipId.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class OwnershipId(p.MessageType): + MESSAGE_WIRE_TYPE = 44 + + def __init__( + self, + *, + ownership_id: bytes, + ) -> None: + self.ownership_id = ownership_id + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('ownership_id', p.BytesType, p.FLAG_REQUIRED), + } diff --git a/hwilib/devices/trezorlib/messages/OwnershipProof.py b/hwilib/devices/trezorlib/messages/OwnershipProof.py new file mode 100644 index 000000000..eca7fbfb1 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/OwnershipProof.py @@ -0,0 +1,30 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class OwnershipProof(p.MessageType): + MESSAGE_WIRE_TYPE = 50 + + def __init__( + self, + *, + ownership_proof: bytes, + signature: bytes, + ) -> None: + self.ownership_proof = ownership_proof + self.signature = signature + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('ownership_proof', p.BytesType, p.FLAG_REQUIRED), + 2: ('signature', p.BytesType, p.FLAG_REQUIRED), + } diff --git a/hwilib/devices/trezorlib/messages/PassphraseAck.py b/hwilib/devices/trezorlib/messages/PassphraseAck.py index 993aa0eb7..9370e6914 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseAck.py +++ b/hwilib/devices/trezorlib/messages/PassphraseAck.py @@ -2,12 +2,20 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class PassphraseAck(p.MessageType): MESSAGE_WIRE_TYPE = 42 def __init__( self, + *, passphrase: str = None, _state: bytes = None, on_device: bool = None, @@ -17,9 +25,9 @@ def __init__( self.on_device = on_device @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('passphrase', p.UnicodeType, 0), - 2: ('_state', p.BytesType, 0), - 3: ('on_device', p.BoolType, 0), + 1: ('passphrase', p.UnicodeType, None), + 2: ('_state', p.BytesType, None), + 3: ('on_device', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/PassphraseRequest.py b/hwilib/devices/trezorlib/messages/PassphraseRequest.py index b4f09efe3..c65034a7a 100644 --- a/hwilib/devices/trezorlib/messages/PassphraseRequest.py +++ b/hwilib/devices/trezorlib/messages/PassphraseRequest.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class PassphraseRequest(p.MessageType): MESSAGE_WIRE_TYPE = 41 def __init__( self, + *, _on_device: bool = None, ) -> None: self._on_device = _on_device @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('_on_device', p.BoolType, 0), + 1: ('_on_device', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/PassphraseSourceType.py b/hwilib/devices/trezorlib/messages/PassphraseSourceType.py deleted file mode 100644 index 3bce46d10..000000000 --- a/hwilib/devices/trezorlib/messages/PassphraseSourceType.py +++ /dev/null @@ -1,5 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -ASK = 0 -DEVICE = 1 -HOST = 2 diff --git a/hwilib/devices/trezorlib/messages/PinMatrixAck.py b/hwilib/devices/trezorlib/messages/PinMatrixAck.py index be2115585..910988cf1 100644 --- a/hwilib/devices/trezorlib/messages/PinMatrixAck.py +++ b/hwilib/devices/trezorlib/messages/PinMatrixAck.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class PinMatrixAck(p.MessageType): MESSAGE_WIRE_TYPE = 19 def __init__( self, - pin: str = None, + *, + pin: str, ) -> None: self.pin = pin @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('pin', p.UnicodeType, 0), # required + 1: ('pin', p.UnicodeType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/PinMatrixRequest.py b/hwilib/devices/trezorlib/messages/PinMatrixRequest.py index db4433b35..5d3d5a194 100644 --- a/hwilib/devices/trezorlib/messages/PinMatrixRequest.py +++ b/hwilib/devices/trezorlib/messages/PinMatrixRequest.py @@ -2,18 +2,27 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypePinMatrixRequestType = Literal[1, 2, 3, 4, 5] + except ImportError: + pass + class PinMatrixRequest(p.MessageType): MESSAGE_WIRE_TYPE = 18 def __init__( self, - type: int = None, + *, + type: EnumTypePinMatrixRequestType = None, ) -> None: self.type = type @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('type', p.UVarintType, 0), + 1: ('type', p.EnumType("PinMatrixRequestType", (1, 2, 3, 4, 5)), None), } diff --git a/hwilib/devices/trezorlib/messages/PinMatrixRequestType.py b/hwilib/devices/trezorlib/messages/PinMatrixRequestType.py index 2010103cb..ded2c04c8 100644 --- a/hwilib/devices/trezorlib/messages/PinMatrixRequestType.py +++ b/hwilib/devices/trezorlib/messages/PinMatrixRequestType.py @@ -1,5 +1,13 @@ # Automatically generated by pb2py # fmt: off -Current = 1 -NewFirst = 2 -NewSecond = 3 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Current: Literal[1] = 1 +NewFirst: Literal[2] = 2 +NewSecond: Literal[3] = 3 +WipeCodeFirst: Literal[4] = 4 +WipeCodeSecond: Literal[5] = 5 diff --git a/hwilib/devices/trezorlib/messages/Ping.py b/hwilib/devices/trezorlib/messages/Ping.py index cd8c71bea..2f3f77632 100644 --- a/hwilib/devices/trezorlib/messages/Ping.py +++ b/hwilib/devices/trezorlib/messages/Ping.py @@ -2,27 +2,29 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Ping(p.MessageType): MESSAGE_WIRE_TYPE = 1 def __init__( self, + *, message: str = None, button_protection: bool = None, - pin_protection: bool = None, - passphrase_protection: bool = None, ) -> None: self.message = message self.button_protection = button_protection - self.pin_protection = pin_protection - self.passphrase_protection = passphrase_protection @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('message', p.UnicodeType, 0), - 2: ('button_protection', p.BoolType, 0), - 3: ('pin_protection', p.BoolType, 0), - 4: ('passphrase_protection', p.BoolType, 0), + 1: ('message', p.UnicodeType, None), + 2: ('button_protection', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/PreauthorizedRequest.py b/hwilib/devices/trezorlib/messages/PreauthorizedRequest.py new file mode 100644 index 000000000..d65af252e --- /dev/null +++ b/hwilib/devices/trezorlib/messages/PreauthorizedRequest.py @@ -0,0 +1,14 @@ +# Automatically generated by pb2py +# fmt: off +from .. import protobuf as p + +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + + +class PreauthorizedRequest(p.MessageType): + MESSAGE_WIRE_TYPE = 85 diff --git a/hwilib/devices/trezorlib/messages/PublicKey.py b/hwilib/devices/trezorlib/messages/PublicKey.py index 08061feb4..09294b904 100644 --- a/hwilib/devices/trezorlib/messages/PublicKey.py +++ b/hwilib/devices/trezorlib/messages/PublicKey.py @@ -4,21 +4,32 @@ from .HDNodeType import HDNodeType +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class PublicKey(p.MessageType): MESSAGE_WIRE_TYPE = 12 def __init__( self, + *, node: HDNodeType = None, xpub: str = None, + root_fingerprint: int = None, ) -> None: self.node = node self.xpub = xpub + self.root_fingerprint = root_fingerprint @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('node', HDNodeType, 0), # required - 2: ('xpub', p.UnicodeType, 0), + 1: ('node', HDNodeType, None), + 2: ('xpub', p.UnicodeType, None), + 3: ('root_fingerprint', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/RecoveryDevice.py b/hwilib/devices/trezorlib/messages/RecoveryDevice.py index 5b6bb8e97..a4f6b69bc 100644 --- a/hwilib/devices/trezorlib/messages/RecoveryDevice.py +++ b/hwilib/devices/trezorlib/messages/RecoveryDevice.py @@ -2,19 +2,28 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeRecoveryDeviceType = Literal[0, 1] + except ImportError: + pass + class RecoveryDevice(p.MessageType): MESSAGE_WIRE_TYPE = 45 def __init__( self, + *, word_count: int = None, passphrase_protection: bool = None, pin_protection: bool = None, language: str = None, label: str = None, enforce_wordlist: bool = None, - type: int = None, + type: EnumTypeRecoveryDeviceType = None, u2f_counter: int = None, dry_run: bool = None, ) -> None: @@ -29,15 +38,15 @@ def __init__( self.dry_run = dry_run @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('word_count', p.UVarintType, 0), - 2: ('passphrase_protection', p.BoolType, 0), - 3: ('pin_protection', p.BoolType, 0), - 4: ('language', p.UnicodeType, 0), # default=english - 5: ('label', p.UnicodeType, 0), - 6: ('enforce_wordlist', p.BoolType, 0), - 8: ('type', p.UVarintType, 0), - 9: ('u2f_counter', p.UVarintType, 0), - 10: ('dry_run', p.BoolType, 0), + 1: ('word_count', p.UVarintType, None), + 2: ('passphrase_protection', p.BoolType, None), + 3: ('pin_protection', p.BoolType, None), + 4: ('language', p.UnicodeType, None), + 5: ('label', p.UnicodeType, None), + 6: ('enforce_wordlist', p.BoolType, None), + 8: ('type', p.EnumType("RecoveryDeviceType", (0, 1)), None), + 9: ('u2f_counter', p.UVarintType, None), + 10: ('dry_run', p.BoolType, None), } diff --git a/hwilib/devices/trezorlib/messages/RecoveryDeviceType.py b/hwilib/devices/trezorlib/messages/RecoveryDeviceType.py index f6f643ac7..965423292 100644 --- a/hwilib/devices/trezorlib/messages/RecoveryDeviceType.py +++ b/hwilib/devices/trezorlib/messages/RecoveryDeviceType.py @@ -1,4 +1,10 @@ # Automatically generated by pb2py # fmt: off -ScrambledWords = 0 -Matrix = 1 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +ScrambledWords: Literal[0] = 0 +Matrix: Literal[1] = 1 diff --git a/hwilib/devices/trezorlib/messages/RequestType.py b/hwilib/devices/trezorlib/messages/RequestType.py index 4c122c944..baffc605c 100644 --- a/hwilib/devices/trezorlib/messages/RequestType.py +++ b/hwilib/devices/trezorlib/messages/RequestType.py @@ -1,7 +1,15 @@ # Automatically generated by pb2py # fmt: off -TXINPUT = 0 -TXOUTPUT = 1 -TXMETA = 2 -TXFINISHED = 3 -TXEXTRADATA = 4 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +TXINPUT: Literal[0] = 0 +TXOUTPUT: Literal[1] = 1 +TXMETA: Literal[2] = 2 +TXFINISHED: Literal[3] = 3 +TXEXTRADATA: Literal[4] = 4 +TXORIGINPUT: Literal[5] = 5 +TXORIGOUTPUT: Literal[6] = 6 diff --git a/hwilib/devices/trezorlib/messages/ResetDevice.py b/hwilib/devices/trezorlib/messages/ResetDevice.py index 2087fa24c..e8a29d524 100644 --- a/hwilib/devices/trezorlib/messages/ResetDevice.py +++ b/hwilib/devices/trezorlib/messages/ResetDevice.py @@ -2,21 +2,31 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeBackupType = Literal[0, 1, 2] + except ImportError: + pass + class ResetDevice(p.MessageType): MESSAGE_WIRE_TYPE = 14 def __init__( self, + *, display_random: bool = None, - strength: int = None, + strength: int = 256, passphrase_protection: bool = None, pin_protection: bool = None, - language: str = None, + language: str = "en-US", label: str = None, - # u2f_counter: int = None, - # skip_backup: bool = None, - # no_backup: bool = None, + u2f_counter: int = None, + skip_backup: bool = None, + no_backup: bool = None, + backup_type: EnumTypeBackupType = 0, ) -> None: self.display_random = display_random self.strength = strength @@ -24,20 +34,22 @@ def __init__( self.pin_protection = pin_protection self.language = language self.label = label - # self.u2f_counter = u2f_counter - # self.skip_backup = skip_backup - # self.no_backup = no_backup + self.u2f_counter = u2f_counter + self.skip_backup = skip_backup + self.no_backup = no_backup + self.backup_type = backup_type @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('display_random', p.BoolType, 0), - 2: ('strength', p.UVarintType, 0), # default=256 - 3: ('passphrase_protection', p.BoolType, 0), - 4: ('pin_protection', p.BoolType, 0), - 5: ('language', p.UnicodeType, 0), # default=english - 6: ('label', p.UnicodeType, 0), - # 7: ('u2f_counter', p.UVarintType, 0), - # 8: ('skip_backup', p.BoolType, 0), - # 9: ('no_backup', p.BoolType, 0), + 1: ('display_random', p.BoolType, None), + 2: ('strength', p.UVarintType, 256), # default=256 + 3: ('passphrase_protection', p.BoolType, None), + 4: ('pin_protection', p.BoolType, None), + 5: ('language', p.UnicodeType, "en-US"), # default=en-US + 6: ('label', p.UnicodeType, None), + 7: ('u2f_counter', p.UVarintType, None), + 8: ('skip_backup', p.BoolType, None), + 9: ('no_backup', p.BoolType, None), + 10: ('backup_type', p.EnumType("BackupType", (0, 1, 2)), 0), # default=Bip39 } diff --git a/hwilib/devices/trezorlib/messages/SafetyCheckLevel.py b/hwilib/devices/trezorlib/messages/SafetyCheckLevel.py new file mode 100644 index 000000000..0bc3566c0 --- /dev/null +++ b/hwilib/devices/trezorlib/messages/SafetyCheckLevel.py @@ -0,0 +1,11 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Strict: Literal[0] = 0 +PromptAlways: Literal[1] = 1 +PromptTemporarily: Literal[2] = 2 diff --git a/hwilib/devices/trezorlib/messages/SelfTest.py b/hwilib/devices/trezorlib/messages/SelfTest.py index 4894ed541..4cd7d20c8 100644 --- a/hwilib/devices/trezorlib/messages/SelfTest.py +++ b/hwilib/devices/trezorlib/messages/SelfTest.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class SelfTest(p.MessageType): MESSAGE_WIRE_TYPE = 32 def __init__( self, + *, payload: bytes = None, ) -> None: self.payload = payload @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('payload', p.BytesType, 0), + 1: ('payload', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/SignIdentity.py b/hwilib/devices/trezorlib/messages/SignIdentity.py deleted file mode 100644 index 0d4579a35..000000000 --- a/hwilib/devices/trezorlib/messages/SignIdentity.py +++ /dev/null @@ -1,30 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -from .. import protobuf as p - -from .IdentityType import IdentityType - - -class SignIdentity(p.MessageType): - MESSAGE_WIRE_TYPE = 53 - - def __init__( - self, - identity: IdentityType = None, - challenge_hidden: bytes = None, - challenge_visual: str = None, - ecdsa_curve_name: str = None, - ) -> None: - self.identity = identity - self.challenge_hidden = challenge_hidden - self.challenge_visual = challenge_visual - self.ecdsa_curve_name = ecdsa_curve_name - - @classmethod - def get_fields(cls): - return { - 1: ('identity', IdentityType, 0), - 2: ('challenge_hidden', p.BytesType, 0), - 3: ('challenge_visual', p.UnicodeType, 0), - 4: ('ecdsa_curve_name', p.UnicodeType, 0), - } diff --git a/hwilib/devices/trezorlib/messages/SignMessage.py b/hwilib/devices/trezorlib/messages/SignMessage.py index 95cbb1595..7e20ad346 100644 --- a/hwilib/devices/trezorlib/messages/SignMessage.py +++ b/hwilib/devices/trezorlib/messages/SignMessage.py @@ -4,9 +4,11 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] except ImportError: - List = None # type: ignore + pass class SignMessage(p.MessageType): @@ -14,10 +16,11 @@ class SignMessage(p.MessageType): def __init__( self, + *, + message: bytes, address_n: List[int] = None, - message: bytes = None, - coin_name: str = None, - script_type: int = None, + coin_name: str = "Bitcoin", + script_type: EnumTypeInputScriptType = 0, ) -> None: self.address_n = address_n if address_n is not None else [] self.message = message @@ -25,10 +28,10 @@ def __init__( self.script_type = script_type @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), - 2: ('message', p.BytesType, 0), # required - 3: ('coin_name', p.UnicodeType, 0), # default=Bitcoin - 4: ('script_type', p.UVarintType, 0), # default=SPENDADDRESS + 2: ('message', p.BytesType, p.FLAG_REQUIRED), + 3: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 4: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS } diff --git a/hwilib/devices/trezorlib/messages/SignTx.py b/hwilib/devices/trezorlib/messages/SignTx.py index db637f1d1..4a656d228 100644 --- a/hwilib/devices/trezorlib/messages/SignTx.py +++ b/hwilib/devices/trezorlib/messages/SignTx.py @@ -2,21 +2,32 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeAmountUnit = Literal[0, 1, 2, 3] + except ImportError: + pass + class SignTx(p.MessageType): MESSAGE_WIRE_TYPE = 15 def __init__( self, - outputs_count: int = None, - inputs_count: int = None, - coin_name: str = None, - version: int = None, - lock_time: int = None, + *, + outputs_count: int, + inputs_count: int, + coin_name: str = "Bitcoin", + version: int = 1, + lock_time: int = 0, expiry: int = None, overwintered: bool = None, version_group_id: int = None, timestamp: int = None, + branch_id: int = None, + amount_unit: EnumTypeAmountUnit = 0, ) -> None: self.outputs_count = outputs_count self.inputs_count = inputs_count @@ -27,17 +38,21 @@ def __init__( self.overwintered = overwintered self.version_group_id = version_group_id self.timestamp = timestamp + self.branch_id = branch_id + self.amount_unit = amount_unit @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('outputs_count', p.UVarintType, 0), # required - 2: ('inputs_count', p.UVarintType, 0), # required - 3: ('coin_name', p.UnicodeType, 0), # default=Bitcoin - 4: ('version', p.UVarintType, 0), # default=1 + 1: ('outputs_count', p.UVarintType, p.FLAG_REQUIRED), + 2: ('inputs_count', p.UVarintType, p.FLAG_REQUIRED), + 3: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin + 4: ('version', p.UVarintType, 1), # default=1 5: ('lock_time', p.UVarintType, 0), # default=0 - 6: ('expiry', p.UVarintType, 0), - 7: ('overwintered', p.BoolType, 0), - 8: ('version_group_id', p.UVarintType, 0), - 9: ('timestamp', p.UVarintType, 0), + 6: ('expiry', p.UVarintType, None), + 7: ('overwintered', p.BoolType, None), + 8: ('version_group_id', p.UVarintType, None), + 9: ('timestamp', p.UVarintType, None), + 10: ('branch_id', p.UVarintType, None), + 11: ('amount_unit', p.EnumType("AmountUnit", (0, 1, 2, 3)), 0), # default=BITCOIN } diff --git a/hwilib/devices/trezorlib/messages/SignedIdentity.py b/hwilib/devices/trezorlib/messages/SignedIdentity.py deleted file mode 100644 index 25bc39277..000000000 --- a/hwilib/devices/trezorlib/messages/SignedIdentity.py +++ /dev/null @@ -1,25 +0,0 @@ -# Automatically generated by pb2py -# fmt: off -from .. import protobuf as p - - -class SignedIdentity(p.MessageType): - MESSAGE_WIRE_TYPE = 54 - - def __init__( - self, - address: str = None, - public_key: bytes = None, - signature: bytes = None, - ) -> None: - self.address = address - self.public_key = public_key - self.signature = signature - - @classmethod - def get_fields(cls): - return { - 1: ('address', p.UnicodeType, 0), - 2: ('public_key', p.BytesType, 0), - 3: ('signature', p.BytesType, 0), - } diff --git a/hwilib/devices/trezorlib/messages/Success.py b/hwilib/devices/trezorlib/messages/Success.py index ec91c8217..d1f58141d 100644 --- a/hwilib/devices/trezorlib/messages/Success.py +++ b/hwilib/devices/trezorlib/messages/Success.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class Success(p.MessageType): MESSAGE_WIRE_TYPE = 2 def __init__( self, + *, message: str = None, ) -> None: self.message = message @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('message', p.UnicodeType, 0), + 1: ('message', p.UnicodeType, None), } diff --git a/hwilib/devices/trezorlib/messages/TransactionType.py b/hwilib/devices/trezorlib/messages/TransactionType.py index 96d25aa41..3f2069843 100644 --- a/hwilib/devices/trezorlib/messages/TransactionType.py +++ b/hwilib/devices/trezorlib/messages/TransactionType.py @@ -8,20 +8,22 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 except ImportError: - List = None # type: ignore + pass class TransactionType(p.MessageType): def __init__( self, - version: int = None, + *, inputs: List[TxInputType] = None, bin_outputs: List[TxOutputBinType] = None, - lock_time: int = None, outputs: List[TxOutputType] = None, + version: int = None, + lock_time: int = None, inputs_cnt: int = None, outputs_cnt: int = None, extra_data: bytes = None, @@ -30,12 +32,13 @@ def __init__( overwintered: bool = None, version_group_id: int = None, timestamp: int = None, + branch_id: int = None, ) -> None: - self.version = version self.inputs = inputs if inputs is not None else [] self.bin_outputs = bin_outputs if bin_outputs is not None else [] - self.lock_time = lock_time self.outputs = outputs if outputs is not None else [] + self.version = version + self.lock_time = lock_time self.inputs_cnt = inputs_cnt self.outputs_cnt = outputs_cnt self.extra_data = extra_data @@ -44,21 +47,23 @@ def __init__( self.overwintered = overwintered self.version_group_id = version_group_id self.timestamp = timestamp + self.branch_id = branch_id @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('version', p.UVarintType, 0), + 1: ('version', p.UVarintType, None), 2: ('inputs', TxInputType, p.FLAG_REPEATED), 3: ('bin_outputs', TxOutputBinType, p.FLAG_REPEATED), - 4: ('lock_time', p.UVarintType, 0), + 4: ('lock_time', p.UVarintType, None), 5: ('outputs', TxOutputType, p.FLAG_REPEATED), - 6: ('inputs_cnt', p.UVarintType, 0), - 7: ('outputs_cnt', p.UVarintType, 0), - 8: ('extra_data', p.BytesType, 0), - 9: ('extra_data_len', p.UVarintType, 0), - 10: ('expiry', p.UVarintType, 0), - 11: ('overwintered', p.BoolType, 0), - 12: ('version_group_id', p.UVarintType, 0), - 13: ('timestamp', p.UVarintType, 0), + 6: ('inputs_cnt', p.UVarintType, None), + 7: ('outputs_cnt', p.UVarintType, None), + 8: ('extra_data', p.BytesType, None), + 9: ('extra_data_len', p.UVarintType, None), + 10: ('expiry', p.UVarintType, None), + 11: ('overwintered', p.BoolType, None), + 12: ('version_group_id', p.UVarintType, None), + 13: ('timestamp', p.UVarintType, None), + 14: ('branch_id', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxAck.py b/hwilib/devices/trezorlib/messages/TxAck.py index 8594b77c1..99fa626a6 100644 --- a/hwilib/devices/trezorlib/messages/TxAck.py +++ b/hwilib/devices/trezorlib/messages/TxAck.py @@ -4,18 +4,26 @@ from .TransactionType import TransactionType +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class TxAck(p.MessageType): MESSAGE_WIRE_TYPE = 22 def __init__( self, + *, tx: TransactionType = None, ) -> None: self.tx = tx @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('tx', TransactionType, 0), + 1: ('tx', TransactionType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxInputType.py b/hwilib/devices/trezorlib/messages/TxInputType.py index 0f1c97d25..70708a31c 100644 --- a/hwilib/devices/trezorlib/messages/TxInputType.py +++ b/hwilib/devices/trezorlib/messages/TxInputType.py @@ -6,27 +6,32 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] except ImportError: - List = None # type: ignore + pass class TxInputType(p.MessageType): def __init__( self, + *, + prev_hash: bytes, + prev_index: int, address_n: List[int] = None, - prev_hash: bytes = None, - prev_index: int = None, script_sig: bytes = None, - sequence: int = None, - script_type: int = None, + sequence: int = 4294967295, + script_type: EnumTypeInputScriptType = 0, multisig: MultisigRedeemScriptType = None, amount: int = None, decred_tree: int = None, - decred_script_version: int = None, - prev_block_hash_bip115: bytes = None, - prev_block_height_bip115: int = None, + witness: bytes = None, + ownership_proof: bytes = None, + commitment_data: bytes = None, + orig_hash: bytes = None, + orig_index: int = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.prev_hash = prev_hash @@ -37,23 +42,27 @@ def __init__( self.multisig = multisig self.amount = amount self.decred_tree = decred_tree - self.decred_script_version = decred_script_version - self.prev_block_hash_bip115 = prev_block_hash_bip115 - self.prev_block_height_bip115 = prev_block_height_bip115 + self.witness = witness + self.ownership_proof = ownership_proof + self.commitment_data = commitment_data + self.orig_hash = orig_hash + self.orig_index = orig_index @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), - 2: ('prev_hash', p.BytesType, 0), # required - 3: ('prev_index', p.UVarintType, 0), # required - 4: ('script_sig', p.BytesType, 0), - 5: ('sequence', p.UVarintType, 0), # default=4294967295 - 6: ('script_type', p.UVarintType, 0), # default=SPENDADDRESS - 7: ('multisig', MultisigRedeemScriptType, 0), - 8: ('amount', p.UVarintType, 0), - 9: ('decred_tree', p.UVarintType, 0), - 10: ('decred_script_version', p.UVarintType, 0), - 11: ('prev_block_hash_bip115', p.BytesType, 0), - 12: ('prev_block_height_bip115', p.UVarintType, 0), + 2: ('prev_hash', p.BytesType, p.FLAG_REQUIRED), + 3: ('prev_index', p.UVarintType, p.FLAG_REQUIRED), + 4: ('script_sig', p.BytesType, None), + 5: ('sequence', p.UVarintType, 4294967295), # default=4294967295 + 6: ('script_type', p.EnumType("InputScriptType", (0, 1, 2, 3, 4)), 0), # default=SPENDADDRESS + 7: ('multisig', MultisigRedeemScriptType, None), + 8: ('amount', p.UVarintType, None), + 9: ('decred_tree', p.UVarintType, None), + 13: ('witness', p.BytesType, None), + 14: ('ownership_proof', p.BytesType, None), + 15: ('commitment_data', p.BytesType, None), + 16: ('orig_hash', p.BytesType, None), + 17: ('orig_index', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxOutputBinType.py b/hwilib/devices/trezorlib/messages/TxOutputBinType.py index c979abdfc..edcaaee88 100644 --- a/hwilib/devices/trezorlib/messages/TxOutputBinType.py +++ b/hwilib/devices/trezorlib/messages/TxOutputBinType.py @@ -2,13 +2,21 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class TxOutputBinType(p.MessageType): def __init__( self, - amount: int = None, - script_pubkey: bytes = None, + *, + amount: int, + script_pubkey: bytes, decred_script_version: int = None, ) -> None: self.amount = amount @@ -16,9 +24,9 @@ def __init__( self.decred_script_version = decred_script_version @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('amount', p.UVarintType, 0), # required - 2: ('script_pubkey', p.BytesType, 0), # required - 3: ('decred_script_version', p.UVarintType, 0), + 1: ('amount', p.UVarintType, p.FLAG_REQUIRED), + 2: ('script_pubkey', p.BytesType, p.FLAG_REQUIRED), + 3: ('decred_script_version', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxOutputType.py b/hwilib/devices/trezorlib/messages/TxOutputType.py index fdc226e85..7da908d4a 100644 --- a/hwilib/devices/trezorlib/messages/TxOutputType.py +++ b/hwilib/devices/trezorlib/messages/TxOutputType.py @@ -6,45 +6,45 @@ if __debug__: try: - from typing import List + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeOutputScriptType = Literal[0, 1, 2, 3, 4, 5] except ImportError: - List = None # type: ignore + pass class TxOutputType(p.MessageType): def __init__( self, - address: str = None, + *, + amount: int, address_n: List[int] = None, - amount: int = None, - script_type: int = None, + address: str = None, + script_type: EnumTypeOutputScriptType = 0, multisig: MultisigRedeemScriptType = None, op_return_data: bytes = None, - decred_script_version: int = None, - block_hash_bip115: bytes = None, - block_height_bip115: int = None, + orig_hash: bytes = None, + orig_index: int = None, ) -> None: - self.address = address self.address_n = address_n if address_n is not None else [] self.amount = amount + self.address = address self.script_type = script_type self.multisig = multisig self.op_return_data = op_return_data - self.decred_script_version = decred_script_version - self.block_hash_bip115 = block_hash_bip115 - self.block_height_bip115 = block_height_bip115 + self.orig_hash = orig_hash + self.orig_index = orig_index @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UnicodeType, 0), + 1: ('address', p.UnicodeType, None), 2: ('address_n', p.UVarintType, p.FLAG_REPEATED), - 3: ('amount', p.UVarintType, 0), # required - 4: ('script_type', p.UVarintType, 0), # required - 5: ('multisig', MultisigRedeemScriptType, 0), - 6: ('op_return_data', p.BytesType, 0), - 7: ('decred_script_version', p.UVarintType, 0), - 8: ('block_hash_bip115', p.BytesType, 0), - 9: ('block_height_bip115', p.UVarintType, 0), + 3: ('amount', p.UVarintType, p.FLAG_REQUIRED), + 4: ('script_type', p.EnumType("OutputScriptType", (0, 1, 2, 3, 4, 5)), 0), # default=PAYTOADDRESS + 5: ('multisig', MultisigRedeemScriptType, None), + 6: ('op_return_data', p.BytesType, None), + 10: ('orig_hash', p.BytesType, None), + 11: ('orig_index', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxRequest.py b/hwilib/devices/trezorlib/messages/TxRequest.py index b619882a9..b3229239d 100644 --- a/hwilib/devices/trezorlib/messages/TxRequest.py +++ b/hwilib/devices/trezorlib/messages/TxRequest.py @@ -5,13 +5,22 @@ from .TxRequestDetailsType import TxRequestDetailsType from .TxRequestSerializedType import TxRequestSerializedType +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeRequestType = Literal[0, 1, 2, 3, 4, 5, 6] + except ImportError: + pass + class TxRequest(p.MessageType): MESSAGE_WIRE_TYPE = 21 def __init__( self, - request_type: int = None, + *, + request_type: EnumTypeRequestType = None, details: TxRequestDetailsType = None, serialized: TxRequestSerializedType = None, ) -> None: @@ -20,9 +29,9 @@ def __init__( self.serialized = serialized @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('request_type', p.UVarintType, 0), - 2: ('details', TxRequestDetailsType, 0), - 3: ('serialized', TxRequestSerializedType, 0), + 1: ('request_type', p.EnumType("RequestType", (0, 1, 2, 3, 4, 5, 6)), None), + 2: ('details', TxRequestDetailsType, None), + 3: ('serialized', TxRequestSerializedType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxRequestDetailsType.py b/hwilib/devices/trezorlib/messages/TxRequestDetailsType.py index 6407b48f8..9c4c9fb4e 100644 --- a/hwilib/devices/trezorlib/messages/TxRequestDetailsType.py +++ b/hwilib/devices/trezorlib/messages/TxRequestDetailsType.py @@ -2,11 +2,19 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class TxRequestDetailsType(p.MessageType): def __init__( self, + *, request_index: int = None, tx_hash: bytes = None, extra_data_len: int = None, @@ -18,10 +26,10 @@ def __init__( self.extra_data_offset = extra_data_offset @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('request_index', p.UVarintType, 0), - 2: ('tx_hash', p.BytesType, 0), - 3: ('extra_data_len', p.UVarintType, 0), - 4: ('extra_data_offset', p.UVarintType, 0), + 1: ('request_index', p.UVarintType, None), + 2: ('tx_hash', p.BytesType, None), + 3: ('extra_data_len', p.UVarintType, None), + 4: ('extra_data_offset', p.UVarintType, None), } diff --git a/hwilib/devices/trezorlib/messages/TxRequestSerializedType.py b/hwilib/devices/trezorlib/messages/TxRequestSerializedType.py index c09b496ff..179698032 100644 --- a/hwilib/devices/trezorlib/messages/TxRequestSerializedType.py +++ b/hwilib/devices/trezorlib/messages/TxRequestSerializedType.py @@ -2,11 +2,19 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class TxRequestSerializedType(p.MessageType): def __init__( self, + *, signature_index: int = None, signature: bytes = None, serialized_tx: bytes = None, @@ -16,9 +24,9 @@ def __init__( self.serialized_tx = serialized_tx @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('signature_index', p.UVarintType, 0), - 2: ('signature', p.BytesType, 0), - 3: ('serialized_tx', p.BytesType, 0), + 1: ('signature_index', p.UVarintType, None), + 2: ('signature', p.BytesType, None), + 3: ('serialized_tx', p.BytesType, None), } diff --git a/hwilib/devices/trezorlib/messages/VerifyMessage.py b/hwilib/devices/trezorlib/messages/VerifyMessage.py index 58447dac3..c24505b11 100644 --- a/hwilib/devices/trezorlib/messages/VerifyMessage.py +++ b/hwilib/devices/trezorlib/messages/VerifyMessage.py @@ -2,16 +2,24 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class VerifyMessage(p.MessageType): MESSAGE_WIRE_TYPE = 39 def __init__( self, - address: str = None, - signature: bytes = None, - message: bytes = None, - coin_name: str = None, + *, + address: str, + signature: bytes, + message: bytes, + coin_name: str = "Bitcoin", ) -> None: self.address = address self.signature = signature @@ -19,10 +27,10 @@ def __init__( self.coin_name = coin_name @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('address', p.UnicodeType, 0), - 2: ('signature', p.BytesType, 0), - 3: ('message', p.BytesType, 0), - 4: ('coin_name', p.UnicodeType, 0), # default=Bitcoin + 1: ('address', p.UnicodeType, p.FLAG_REQUIRED), + 2: ('signature', p.BytesType, p.FLAG_REQUIRED), + 3: ('message', p.BytesType, p.FLAG_REQUIRED), + 4: ('coin_name', p.UnicodeType, "Bitcoin"), # default=Bitcoin } diff --git a/hwilib/devices/trezorlib/messages/WipeDevice.py b/hwilib/devices/trezorlib/messages/WipeDevice.py index f695def18..576318128 100644 --- a/hwilib/devices/trezorlib/messages/WipeDevice.py +++ b/hwilib/devices/trezorlib/messages/WipeDevice.py @@ -2,6 +2,13 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class WipeDevice(p.MessageType): MESSAGE_WIRE_TYPE = 5 diff --git a/hwilib/devices/trezorlib/messages/WordAck.py b/hwilib/devices/trezorlib/messages/WordAck.py index 1ef36958a..fb912debf 100644 --- a/hwilib/devices/trezorlib/messages/WordAck.py +++ b/hwilib/devices/trezorlib/messages/WordAck.py @@ -2,18 +2,26 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + class WordAck(p.MessageType): MESSAGE_WIRE_TYPE = 47 def __init__( self, - word: str = None, + *, + word: str, ) -> None: self.word = word @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('word', p.UnicodeType, 0), # required + 1: ('word', p.UnicodeType, p.FLAG_REQUIRED), } diff --git a/hwilib/devices/trezorlib/messages/WordRequest.py b/hwilib/devices/trezorlib/messages/WordRequest.py index 7c14830db..1fc4d3880 100644 --- a/hwilib/devices/trezorlib/messages/WordRequest.py +++ b/hwilib/devices/trezorlib/messages/WordRequest.py @@ -2,18 +2,27 @@ # fmt: off from .. import protobuf as p +if __debug__: + try: + from typing import Dict, List # noqa: F401 + from typing_extensions import Literal # noqa: F401 + EnumTypeWordRequestType = Literal[0, 1, 2] + except ImportError: + pass + class WordRequest(p.MessageType): MESSAGE_WIRE_TYPE = 46 def __init__( self, - type: int = None, + *, + type: EnumTypeWordRequestType = None, ) -> None: self.type = type @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict: return { - 1: ('type', p.UVarintType, 0), + 1: ('type', p.EnumType("WordRequestType", (0, 1, 2)), None), } diff --git a/hwilib/devices/trezorlib/messages/WordRequestType.py b/hwilib/devices/trezorlib/messages/WordRequestType.py index a284fac9c..4004fb14a 100644 --- a/hwilib/devices/trezorlib/messages/WordRequestType.py +++ b/hwilib/devices/trezorlib/messages/WordRequestType.py @@ -1,5 +1,11 @@ # Automatically generated by pb2py # fmt: off -Plain = 0 -Matrix9 = 1 -Matrix6 = 2 +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +Plain: Literal[0] = 0 +Matrix9: Literal[1] = 1 +Matrix6: Literal[2] = 2 diff --git a/hwilib/devices/trezorlib/messages/__init__.py b/hwilib/devices/trezorlib/messages/__init__.py index 9a4ad93da..3971a1db2 100644 --- a/hwilib/devices/trezorlib/messages/__init__.py +++ b/hwilib/devices/trezorlib/messages/__init__.py @@ -4,21 +4,30 @@ from .Address import Address from .ApplyFlags import ApplyFlags from .ApplySettings import ApplySettings +from .AuthorizeCoinJoin import AuthorizeCoinJoin from .BackupDevice import BackupDevice from .ButtonAck import ButtonAck from .ButtonRequest import ButtonRequest from .Cancel import Cancel from .ChangePin import ChangePin -from .ClearSession import ClearSession from .DebugLinkDecision import DebugLinkDecision +from .DebugLinkEraseSdCard import DebugLinkEraseSdCard from .DebugLinkFlashErase import DebugLinkFlashErase from .DebugLinkGetState import DebugLinkGetState -from .DebugLinkLog import DebugLinkLog from .DebugLinkMemory import DebugLinkMemory from .DebugLinkMemoryRead import DebugLinkMemoryRead from .DebugLinkMemoryWrite import DebugLinkMemoryWrite +from .DebugLinkRecordScreen import DebugLinkRecordScreen +from .DebugLinkReseedRandom import DebugLinkReseedRandom +from .DebugLinkShowText import DebugLinkShowText +from .DebugLinkShowTextItem import DebugLinkShowTextItem from .DebugLinkState import DebugLinkState from .DebugLinkStop import DebugLinkStop +from .DebugLinkWatchLayout import DebugLinkWatchLayout +from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck +from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest +from .DoPreauthorized import DoPreauthorized +from .EndSession import EndSession from .Entropy import Entropy from .EntropyAck import EntropyAck from .EntropyRequest import EntropyRequest @@ -30,29 +39,30 @@ from .GetAddress import GetAddress from .GetEntropy import GetEntropy from .GetFeatures import GetFeatures +from .GetOwnershipId import GetOwnershipId +from .GetOwnershipProof import GetOwnershipProof from .GetPublicKey import GetPublicKey from .HDNodePathType import HDNodePathType from .HDNodeType import HDNodeType -from .IdentityType import IdentityType from .Initialize import Initialize from .LoadDevice import LoadDevice +from .LockDevice import LockDevice from .MessageSignature import MessageSignature from .MultisigRedeemScriptType import MultisigRedeemScriptType +from .OwnershipId import OwnershipId +from .OwnershipProof import OwnershipProof from .PassphraseAck import PassphraseAck from .PassphraseRequest import PassphraseRequest -from .Deprecated_PassphraseStateAck import Deprecated_PassphraseStateAck -from .Deprecated_PassphraseStateRequest import Deprecated_PassphraseStateRequest from .PinMatrixAck import PinMatrixAck from .PinMatrixRequest import PinMatrixRequest from .Ping import Ping +from .PreauthorizedRequest import PreauthorizedRequest from .PublicKey import PublicKey from .RecoveryDevice import RecoveryDevice from .ResetDevice import ResetDevice from .SelfTest import SelfTest -from .SignIdentity import SignIdentity from .SignMessage import SignMessage from .SignTx import SignTx -from .SignedIdentity import SignedIdentity from .Success import Success from .TransactionType import TransactionType from .TxAck import TxAck @@ -66,13 +76,18 @@ from .WipeDevice import WipeDevice from .WordAck import WordAck from .WordRequest import WordRequest +from . import AmountUnit +from . import BackupType from . import ButtonRequestType +from . import Capability +from . import DebugLinkShowTextStyle +from . import DebugSwipeDirection from . import FailureType from . import InputScriptType from . import MessageType from . import OutputScriptType -from . import PassphraseSourceType from . import PinMatrixRequestType from . import RecoveryDeviceType from . import RequestType +from . import SafetyCheckLevel from . import WordRequestType diff --git a/hwilib/devices/trezorlib/protobuf.py b/hwilib/devices/trezorlib/protobuf.py index 3001bb1f9..0df86fe80 100644 --- a/hwilib/devices/trezorlib/protobuf.py +++ b/hwilib/devices/trezorlib/protobuf.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,54 +14,90 @@ # You should have received a copy of the License along with this library. # If not, see . -''' -Extremely minimal streaming codec for a subset of protobuf. Supports uint32, -bytes, string, embedded message and repeated fields. +""" +Extremely minimal streaming codec for a subset of protobuf. +Supports uint32, bytes, string, embedded message and repeated fields. -For de-sererializing (loading) protobuf types, object with `Reader` -interface is required: - ->>> class Reader: ->>> def readinto(self, buffer): ->>> """ ->>> Reads `len(buffer)` bytes into `buffer`, or raises `EOFError`. ->>> """ - -For serializing (dumping) protobuf types, object with `Writer` interface is -required: - ->>> class Writer: ->>> def write(self, buffer): ->>> """ ->>> Writes all bytes from `buffer`, or raises `EOFError`. ->>> """ -''' +For de-serializing (loading) protobuf types, object with `Reader` interface is required. +For serializing (dumping) protobuf types, object with `Writer` interface is required. +""" +import logging +import warnings from io import BytesIO -from typing import Any, Optional +from itertools import zip_longest +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) + +from typing_extensions import Protocol + +FieldType = Union[ + "EnumType", + Type["MessageType"], + Type["UVarintType"], + Type["SVarintType"], + Type["BoolType"], + Type["UnicodeType"], + Type["BytesType"], +] +FieldInfo = Tuple[str, FieldType, Any] +MT = TypeVar("MT", bound="MessageType") + + +class Reader(Protocol): + def readinto(self, buffer: bytearray) -> int: + """ + Reads exactly `len(buffer)` bytes into `buffer`. Returns number of bytes read, + or 0 if it cannot read that much. + """ + + +class Writer(Protocol): + def write(self, buffer: bytes) -> int: + """ + Writes all bytes from `buffer`, or raises `EOFError` + """ + _UVARINT_BUFFER = bytearray(1) +LOG = logging.getLogger(__name__) -def load_uvarint(reader): + +def load_uvarint(reader: Reader) -> int: buffer = _UVARINT_BUFFER result = 0 shift = 0 byte = 0x80 + bytes_read = 0 while byte & 0x80: if reader.readinto(buffer) == 0: - raise EOFError + if bytes_read > 0: + raise IOError("Interrupted UVarint") + else: + raise EOFError + bytes_read += 1 byte = buffer[0] result += (byte & 0x7F) << shift shift += 7 return result -def dump_uvarint(writer, n): +def dump_uvarint(writer: Writer, n: int) -> None: if n < 0: raise ValueError("Cannot dump signed value, convert it to unsigned first.") buffer = _UVARINT_BUFFER - shifted = True + shifted = 1 while shifted: shifted = n >> 7 buffer[0] = (n & 0x7F) | (0x80 if shifted else 0x00) @@ -89,14 +125,14 @@ def dump_uvarint(writer, n): # So we have to branch on whether the number is negative. -def sint_to_uint(sint): +def sint_to_uint(sint: int) -> int: res = sint << 1 if sint < 0: res = ~res return res -def uint_to_sint(uint): +def uint_to_sint(uint: int) -> int: sign = uint & 1 res = uint >> 1 if sign: @@ -116,6 +152,45 @@ class BoolType: WIRE_TYPE = 0 +class EnumType: + WIRE_TYPE = 0 + + def __init__(self, enum_name: str, enum_values: Iterable[int]) -> None: + self.enum_name = enum_name + self.enum_values = enum_values + + def validate(self, fvalue: int) -> int: + if fvalue not in self.enum_values: + # raise TypeError("Invalid enum value") + LOG.info("Value {} unknown for type {}".format(fvalue, self.enum_name)) + return fvalue + + def to_str(self, fvalue: int) -> str: + from . import messages + + module = getattr(messages, self.enum_name) + for name in dir(module): + if name.startswith("__"): + continue + if getattr(module, name) == fvalue: + return name + else: + raise TypeError("Invalid enum value") + + def from_str(self, fstr: str) -> int: + try: + from . import messages + + module = getattr(messages, self.enum_name) + ivalue = getattr(module, fstr) + if isinstance(ivalue, int): + return ivalue + else: + raise TypeError("Invalid enum value") + except AttributeError: + raise TypeError("Invalid enum value") from None + + class BytesType: WIRE_TYPE = 2 @@ -124,22 +199,79 @@ class UnicodeType: WIRE_TYPE = 2 -class MessageType: +class _MessageTypeMeta(type): + def __init__(cls, name, bases, d) -> None: + super().__init__(name, bases, d) + if name != "MessageType": + cls.__init__ = MessageType.__init__ + + +class MessageType(metaclass=_MessageTypeMeta): WIRE_TYPE = 2 @classmethod - def get_fields(cls): + def get_fields(cls) -> Dict[int, FieldInfo]: + """Return a field descriptor. + + The descriptor is a mapping: + field_id -> (field_name, field_type, default_value) + + `default_value` can also be one of the special values: + * `FLAG_REQUIRED` indicates that the field value has no default and _must_ be + provided by caller/sender. + * `FLAG_REPEATED` indicates that the field is a list of `field_type` values. In + that case the default value is an empty list. + """ return {} - def __init__(self, **kwargs): - for kw in kwargs: - setattr(self, kw, kwargs[kw]) - self._fill_missing() - - def __eq__(self, rhs): + @classmethod + def get_field_type(cls, name: str) -> Optional[FieldType]: + for fname, ftype, _ in cls.get_fields().values(): + if fname == name: + return ftype + return None + + def __init__(self, *args, **kwargs: Any) -> None: + fields = self.get_fields() + if args: + warnings.warn( + "Positional arguments for MessageType are deprecated", + DeprecationWarning, + stacklevel=2, + ) + # process fields one by one + NOT_PROVIDED = object() + for field, val in zip_longest(fields.values(), args, fillvalue=NOT_PROVIDED): + if field is NOT_PROVIDED: + raise TypeError("too many positional arguments") + fname, _, fdefault = field + if fname in kwargs and val is not NOT_PROVIDED: + # both *args and **kwargs specify the same thing + raise TypeError(f"got multiple values for argument '{fname}'") + elif fname in kwargs: + # set in kwargs but not in args + setattr(self, fname, kwargs[fname]) + elif val is not NOT_PROVIDED: + # set in args but not in kwargs + setattr(self, fname, val) + else: + # not set at all, pick a default + if fdefault is FLAG_REPEATED: + fdefault = [] + elif fdefault is FLAG_EXPERIMENTAL: + fdefault = None + elif fdefault is FLAG_REQUIRED: + warnings.warn( + f"Value of required field '{fname}' must be provided in constructor", + DeprecationWarning, + stacklevel=2, + ) + setattr(self, fname, fdefault) + + def __eq__(self, rhs: Any) -> bool: return self.__class__ is rhs.__class__ and self.__dict__ == rhs.__dict__ - def __repr__(self): + def __repr__(self) -> str: d = {} for key, value in self.__dict__.items(): if value is None or value == []: @@ -147,41 +279,20 @@ def __repr__(self): d[key] = value return "<%s: %s>" % (self.__class__.__name__, d) - def __iter__(self): - return iter(self.keys()) - - def keys(self): - return (name for name, _, _ in self.get_fields().values()) - - def __getitem__(self, key): - return getattr(self, key) - - def _fill_missing(self): - # fill missing fields - for fname, _, fflags in self.get_fields().values(): - if not hasattr(self, fname): - if fflags & FLAG_REPEATED: - setattr(self, fname, []) - else: - setattr(self, fname, None) - - def CopyFrom(self, obj): - self.__dict__ = obj.__dict__.copy() - - def ByteSize(self): + def ByteSize(self) -> int: data = BytesIO() dump_message(data, self) return len(data.getvalue()) class LimitedReader: - def __init__(self, reader, limit): + def __init__(self, reader: Reader, limit: int) -> None: self.reader = reader self.limit = limit - def readinto(self, buf): + def readinto(self, buf: bytearray) -> int: if self.limit < len(buf): - raise EOFError + return 0 else: nread = self.reader.readinto(buf) self.limit -= nread @@ -189,21 +300,76 @@ def readinto(self, buf): class CountingWriter: - def __init__(self): + def __init__(self) -> None: self.size = 0 - def write(self, buf): + def write(self, buf: bytes) -> int: nwritten = len(buf) self.size += nwritten return nwritten -FLAG_REPEATED = 1 +FLAG_REPEATED = object() +FLAG_REQUIRED = object() +FLAG_EXPERIMENTAL = object() -def load_message(reader, msg_type): +def decode_packed_array_field(ftype: FieldType, reader: Reader) -> List[Any]: + length = load_uvarint(reader) + packed_reader = LimitedReader(reader, length) + values = [] + try: + while True: + values.append(decode_varint_field(ftype, packed_reader)) + except EOFError: + pass + return values + + +def decode_varint_field(ftype: FieldType, reader: Reader) -> Union[int, bool]: + value = load_uvarint(reader) + if ftype is UVarintType: + return value + elif ftype is SVarintType: + return uint_to_sint(value) + elif ftype is BoolType: + return bool(value) + elif isinstance(ftype, EnumType): + return ftype.validate(value) + else: + raise TypeError # not a varint field or unknown type + + +def decode_length_delimited_field( + ftype: FieldType, reader: Reader +) -> Union[bytes, str, MessageType]: + value = load_uvarint(reader) + if ftype is BytesType: + buf = bytearray(value) + reader.readinto(buf) + return bytes(buf) + elif ftype is UnicodeType: + buf = bytearray(value) + reader.readinto(buf) + return buf.decode() + elif isinstance(ftype, type) and issubclass(ftype, MessageType): + return load_message(LimitedReader(reader, value), ftype) + else: + raise TypeError # field type is unknown + + +def load_message(reader: Reader, msg_type: Type[MT]) -> MT: fields = msg_type.get_fields() - msg = msg_type() + + msg_dict = {} + # pre-seed the dict + for fname, _, fdefault in fields.values(): + if fdefault is FLAG_REPEATED: + msg_dict[fname] = [] + elif fdefault is FLAG_EXPERIMENTAL: + msg_dict[fname] = None + elif fdefault is not FLAG_REQUIRED: + msg_dict[fname] = fdefault while True: try: @@ -226,55 +392,54 @@ def load_message(reader, msg_type): raise ValueError continue - fname, ftype, fflags = field - if wtype != ftype.WIRE_TYPE: + fname, ftype, fdefault = field + + if wtype == 2 and ftype.WIRE_TYPE == 0 and fdefault is FLAG_REPEATED: + # packed array + fvalues = decode_packed_array_field(ftype, reader) + + elif wtype != ftype.WIRE_TYPE: raise TypeError # parsed wire type differs from the schema - ivalue = load_uvarint(reader) - - if ftype is UVarintType: - fvalue = ivalue - elif ftype is SVarintType: - fvalue = uint_to_sint(ivalue) - elif ftype is BoolType: - fvalue = bool(ivalue) - elif ftype is BytesType: - buf = bytearray(ivalue) - reader.readinto(buf) - fvalue = bytes(buf) - elif ftype is UnicodeType: - buf = bytearray(ivalue) - reader.readinto(buf) - fvalue = buf.decode() - elif issubclass(ftype, MessageType): - fvalue = load_message(LimitedReader(reader, ivalue), ftype) + elif wtype == 2: + fvalues = [decode_length_delimited_field(ftype, reader)] + + elif wtype == 0: + fvalues = [decode_varint_field(ftype, reader)] + else: - raise TypeError # field type is unknown + raise TypeError # unknown wire type - if fflags & FLAG_REPEATED: - pvalue = getattr(msg, fname) - pvalue.append(fvalue) - fvalue = pvalue - setattr(msg, fname, fvalue) + if fdefault is FLAG_REPEATED: + msg_dict[fname].extend(fvalues) + elif len(fvalues) != 1: + raise ValueError("Unexpected multiple values in non-repeating field") + else: + msg_dict[fname] = fvalues[0] - return msg + for fname, _, fdefault in fields.values(): + if fdefault is FLAG_REQUIRED and fname not in msg_dict: + raise ValueError # required field was not received + return msg_type(**msg_dict) -def dump_message(writer, msg): +def dump_message(writer: Writer, msg: MessageType) -> None: repvalue = [0] mtype = msg.__class__ fields = mtype.get_fields() for ftag in fields: - fname, ftype, fflags = fields[ftag] + fname, ftype, fdefault = fields[ftag] fvalue = getattr(msg, fname, None) if fvalue is None: continue + if fvalue is FLAG_REQUIRED: + raise ValueError # required value was not provided fkey = (ftag << 3) | ftype.WIRE_TYPE - if not fflags & FLAG_REPEATED: + if fdefault is not FLAG_REPEATED: repvalue[0] = fvalue fvalue = repvalue @@ -290,16 +455,17 @@ def dump_message(writer, msg): elif ftype is BoolType: dump_uvarint(writer, int(svalue)) + elif isinstance(ftype, EnumType): + dump_uvarint(writer, ftype.validate(svalue)) + elif ftype is BytesType: dump_uvarint(writer, len(svalue)) writer.write(svalue) elif ftype is UnicodeType: - if not isinstance(svalue, bytes): - svalue = svalue.encode() - - dump_uvarint(writer, len(svalue)) - writer.write(svalue) + svalue_bytes = svalue.encode() + dump_uvarint(writer, len(svalue_bytes)) + writer.write(svalue_bytes) elif issubclass(ftype, MessageType): counter = CountingWriter() @@ -318,34 +484,39 @@ def format_message( truncate_after: Optional[int] = 256, truncate_to: Optional[int] = 64, ) -> str: - def mostly_printable(bytes): + def mostly_printable(bytes: bytes) -> bool: if not bytes: return True printable = sum(1 for byte in bytes if 0x20 <= byte <= 0x7E) return printable / len(bytes) > 0.8 - def pformat_value(value: Any, indent: int) -> str: + def pformat(name: str, value: Any, indent: int) -> str: level = sep * indent leadin = sep * (indent + 1) + ftype = pb.get_field_type(name) + if isinstance(value, MessageType): return format_message(value, indent, sep) + if isinstance(value, list): # short list of simple values - if not value or not isinstance(value[0], MessageType): + if not value or ftype in (UVarintType, SVarintType, BoolType): return repr(value) # long list, one line per entry lines = ["[", level + "]"] - lines[1:1] = [leadin + pformat_value(x, indent + 1) + "," for x in value] + lines[1:1] = [leadin + pformat(name, x, indent + 1) + "," for x in value] return "\n".join(lines) + if isinstance(value, dict): lines = ["{"] for key, val in sorted(value.items()): if val is None or val == []: continue - lines.append(leadin + key + ": " + pformat_value(val, indent + 1) + ",") + lines.append(leadin + key + ": " + pformat(key, val, indent + 1) + ",") lines.append(level + "}") return "\n".join(lines) + if isinstance(value, (bytes, bytearray)): length = len(value) suffix = "" @@ -358,19 +529,31 @@ def pformat_value(value: Any, indent: int) -> str: output = "0x" + value.hex() return "{} bytes {}{}".format(length, output, suffix) + if isinstance(value, int) and isinstance(ftype, EnumType): + try: + return "{} ({})".format(ftype.to_str(value), value) + except TypeError: + return str(value) + return repr(value) return "{name} ({size} bytes) {content}".format( name=pb.__class__.__name__, size=pb.ByteSize(), - content=pformat_value(pb.__dict__, indent), + content=pformat("", pb.__dict__, indent), ) -def value_to_proto(ftype, value): - if issubclass(ftype, MessageType): +def value_to_proto(ftype: FieldType, value: Any) -> Any: + if isinstance(ftype, type) and issubclass(ftype, MessageType): raise TypeError("value_to_proto only converts simple values") + if isinstance(ftype, EnumType): + if isinstance(value, str): + return ftype.from_str(value) + else: + return int(value) + if ftype in (UVarintType, SVarintType): return int(value) @@ -389,10 +572,10 @@ def value_to_proto(ftype, value): raise TypeError("can't convert {} value to bytes".format(type(value))) -def dict_to_proto(message_type, d): +def dict_to_proto(message_type: Type[MT], d: Dict[str, Any]) -> MT: params = {} - for fname, ftype, fflags in message_type.get_fields().values(): - repeated = fflags & FLAG_REPEATED + for fname, ftype, fdefault in message_type.get_fields().values(): + repeated = fdefault is FLAG_REPEATED value = d.get(fname) if value is None: continue @@ -400,8 +583,8 @@ def dict_to_proto(message_type, d): if not repeated: value = [value] - if issubclass(ftype, MessageType): - function = dict_to_proto + if isinstance(ftype, type) and issubclass(ftype, MessageType): + function: Callable[[Any, Any], Any] = dict_to_proto else: function = value_to_proto @@ -414,12 +597,26 @@ def dict_to_proto(message_type, d): return message_type(**params) -def to_dict(msg): +def to_dict(msg: MessageType, hexlify_bytes: bool = True) -> Dict[str, Any]: + def convert_value(ftype: FieldType, value: Any) -> Any: + if hexlify_bytes and isinstance(value, bytes): + return value.hex() + elif isinstance(value, MessageType): + return to_dict(value, hexlify_bytes) + elif isinstance(value, list): + return [convert_value(ftype, v) for v in value] + elif isinstance(value, int) and isinstance(ftype, EnumType): + try: + return ftype.to_str(value) + except TypeError: + return value + else: + return value + res = {} for key, value in msg.__dict__.items(): if value is None or value == []: continue - if isinstance(value, MessageType): - value = to_dict(value) - res[key] = value + res[key] = convert_value(msg.get_field_type(key), value) + return res diff --git a/hwilib/devices/trezorlib/tools.py b/hwilib/devices/trezorlib/tools.py index 83552b5f6..35fc09f96 100644 --- a/hwilib/devices/trezorlib/tools.py +++ b/hwilib/devices/trezorlib/tools.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -19,11 +19,7 @@ import re import struct import unicodedata -from typing import List, NewType, Union - -from .exceptions import TrezorFailure - -CallException = TrezorFailure +from typing import List, NewType HARDENED_FLAG = 1 << 31 @@ -44,6 +40,14 @@ def btc_hash(data): return hashlib.sha256(hashlib.sha256(data).digest()).digest() +def tx_hash(data): + """Calculate and return double-SHA256 hash in reverse order. + + This is what Bitcoin uses as txids. + """ + return btc_hash(data)[::-1] + + def hash_160(public_key): md = hashlib.new("ripemd160") md.update(hashlib.sha256(public_key).digest()) @@ -177,17 +181,17 @@ def str_to_harden(x: str) -> int: try: return [str_to_harden(x) for x in n] - except Exception: - raise ValueError("Invalid BIP32 path", nstr) + except Exception as e: + raise ValueError("Invalid BIP32 path", nstr) from e -def normalize_nfc(txt: Union[str, bytes]) -> bytes: +def normalize_nfc(txt): """ Normalize message to NFC and return bytes suitable for protobuf. This seems to be bitcoin-qt standard of doing things. """ if isinstance(txt, bytes): - return txt + txt = txt.decode() return unicodedata.normalize("NFC", txt).encode() @@ -263,3 +267,51 @@ def dict_from_camelcase(d, renames=None): res[newkey] = dict_from_camelcase(value, renames) return res + + +# adapted from https://github.com/bitcoin-core/HWI/blob/master/hwilib/descriptor.py + + +def descriptor_checksum(desc: str) -> str: + def _polymod(c: int, val: int) -> int: + c0 = c >> 35 + c = ((c & 0x7FFFFFFFF) << 5) ^ val + if c0 & 1: + c ^= 0xF5DEE51989 + if c0 & 2: + c ^= 0xA9FDCA3312 + if c0 & 4: + c ^= 0x1BAB10E32D + if c0 & 8: + c ^= 0x3706B1677A + if c0 & 16: + c ^= 0x644D626FFD + return c + + INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " + CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + c = 1 + cls = 0 + clscount = 0 + for ch in desc: + pos = INPUT_CHARSET.find(ch) + if pos == -1: + return "" + c = _polymod(c, pos & 31) + cls = cls * 3 + (pos >> 5) + clscount += 1 + if clscount == 3: + c = _polymod(c, cls) + cls = 0 + clscount = 0 + if clscount > 0: + c = _polymod(c, cls) + for j in range(0, 8): + c = _polymod(c, 0) + c ^= 1 + + ret = [""] * 8 + for j in range(0, 8): + ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] + return "".join(ret) diff --git a/hwilib/devices/trezorlib/transport/__init__.py b/hwilib/devices/trezorlib/transport/__init__.py index 33ff23ba4..6e1e6f178 100644 --- a/hwilib/devices/trezorlib/transport/__init__.py +++ b/hwilib/devices/trezorlib/transport/__init__.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -15,10 +15,9 @@ # If not, see . import logging -from typing import Iterable, List, Type +from typing import Iterable, List, Tuple, Type from ..exceptions import TrezorException -from ..protobuf import MessageType LOG = logging.getLogger(__name__) @@ -26,12 +25,8 @@ DEV_TREZOR1 = (0x534C, 0x0001) DEV_TREZOR2 = (0x1209, 0x53C1) DEV_TREZOR2_BL = (0x1209, 0x53C0) -DEV_KEEPKEY = (0x2B24, 0x0001) -DEV_KEEPKEY_WEBUSB = (0x2B24, 0x0002) -TREZORS = {DEV_TREZOR1, DEV_TREZOR2, DEV_TREZOR2_BL, DEV_KEEPKEY, DEV_KEEPKEY_WEBUSB} -TREZOR_VENDOR_IDS = {0x534C, 0x1209} -KEEPKEY_VENDOR_IDS = {0x2B24} +TREZORS = {DEV_TREZOR1, DEV_TREZOR2, DEV_TREZOR2_BL} UDEV_RULES_STR = """ Do you have udev rules installed? @@ -39,6 +34,9 @@ """.strip() +MessagePayload = Tuple[int, bytes] + + class TransportException(TrezorException): pass @@ -46,7 +44,7 @@ class TransportException(TrezorException): class Transport: """Raw connection to a Trezor device. - Transport subclass represents a kind of communication link: WebUSB + Transport subclass represents a kind of communication link: Trezor Bridge, WebUSB or USB-HID connection, or UDP socket of listening emulator(s). It can also enumerate devices available over this communication link, and return them as instances. @@ -60,7 +58,7 @@ class Transport: a Trezor device to a computer. """ - PATH_PREFIX = None # type: str + PATH_PREFIX: str = None ENABLED = False def __str__(self) -> str: @@ -69,19 +67,16 @@ def __str__(self) -> str: def get_path(self) -> str: raise NotImplementedError - def get_usb_vendor_id(self) -> int: - return -1 - def begin_session(self) -> None: raise NotImplementedError def end_session(self) -> None: raise NotImplementedError - def read(self) -> MessageType: + def read(self) -> MessagePayload: raise NotImplementedError - def write(self, message: MessageType) -> None: + def write(self, message_type: int, message_data: bytes) -> None: raise NotImplementedError @classmethod @@ -104,24 +99,25 @@ def find_by_path(cls, path: str, prefix_search: bool = False) -> "Transport": def all_transports() -> Iterable[Type[Transport]]: + from .bridge import BridgeTransport from .hid import HidTransport from .udp import UdpTransport from .webusb import WebUsbTransport return set( cls - for cls in (HidTransport, UdpTransport, WebUsbTransport) + for cls in (BridgeTransport, HidTransport, UdpTransport, WebUsbTransport) if cls.ENABLED ) def enumerate_devices() -> Iterable[Transport]: - devices = [] # type: List[Transport] + devices: List[Transport] = [] for transport in all_transports(): name = transport.__name__ try: found = list(transport.enumerate()) - LOG.debug("Enumerating {}: found {} devices".format(name, len(found))) + LOG.info("Enumerating {}: found {} devices".format(name, len(found))) devices.extend(found) except NotImplementedError: LOG.error("{} does not implement device enumeration".format(name)) @@ -136,7 +132,7 @@ def get_transport(path: str = None, prefix_search: bool = False) -> Transport: try: return next(iter(enumerate_devices())) except StopIteration: - raise TransportException("No TREZOR device found") from None + raise TransportException("No Trezor device found") from None # Find whether B is prefix of A (transport name is part of the path) # or A is prefix of B (path is a prefix, or a name, of transport). @@ -144,7 +140,7 @@ def get_transport(path: str = None, prefix_search: bool = False) -> Transport: def match_prefix(a: str, b: str) -> bool: return a.startswith(b) or b.startswith(a) - LOG.debug( + LOG.info( "looking for device by {}: {}".format( "prefix" if prefix_search else "full path", path ) diff --git a/hwilib/devices/trezorlib/transport/hid.py b/hwilib/devices/trezorlib/transport/hid.py index e9b816720..4dd120a50 100644 --- a/hwilib/devices/trezorlib/transport/hid.py +++ b/hwilib/devices/trezorlib/transport/hid.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -19,7 +19,8 @@ import time from typing import Any, Dict, Iterable -from . import DEV_TREZOR1, DEV_KEEPKEY, UDEV_RULES_STR, TransportException +from ..log import DUMP_PACKETS +from . import DEV_TREZOR1, UDEV_RULES_STR, TransportException from .protocol import ProtocolBasedTransport, ProtocolV1 LOG = logging.getLogger(__name__) @@ -27,7 +28,7 @@ try: import hid except Exception as e: - LOG.warning("HID transport is disabled: {}".format(e)) + LOG.info("HID transport is disabled: {}".format(e)) hid = None @@ -41,7 +42,7 @@ def __init__( ) -> None: self.path = path self.serial = serial - self.handle = None # type: HidDeviceHandle + self.handle: HidDeviceHandle = None self.hid_version = None if probe_hid_version else 2 def open(self) -> None: @@ -82,17 +83,21 @@ def write_chunk(self, chunk: bytes) -> None: raise TransportException("Unexpected chunk size: %d" % len(chunk)) if self.hid_version == 2: - self.handle.write(b"\0" + bytearray(chunk)) - else: - self.handle.write(chunk) + chunk = b"\x00" + chunk + + LOG.log(DUMP_PACKETS, "writing packet: {}".format(chunk.hex())) + self.handle.write(chunk) def read_chunk(self) -> bytes: while True: - chunk = self.handle.read(64) + # hidapi seems to return lists of ints instead of bytes + chunk = bytes(self.handle.read(64)) if chunk: break else: time.sleep(0.001) + + LOG.log(DUMP_PACKETS, "read packet: {}".format(chunk.hex())) if len(chunk) != 64: raise TransportException("Unexpected chunk size: %d" % len(chunk)) return bytes(chunk) @@ -119,21 +124,17 @@ def __init__(self, device: HidDevice) -> None: self.device = device self.handle = HidHandle(device["path"], device["serial_number"]) - protocol = ProtocolV1(self.handle) - super().__init__(protocol=protocol) + super().__init__(protocol=ProtocolV1(self.handle)) def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, self.device["path"].decode()) - def get_usb_vendor_id(self) -> int: - return self.device["vendor_id"] - @classmethod def enumerate(cls, debug: bool = False) -> Iterable["HidTransport"]: devices = [] for dev in hid.enumerate(0, 0): usb_id = (dev["vendor_id"], dev["product_id"]) - if usb_id != DEV_TREZOR1 and usb_id != DEV_KEEPKEY: + if usb_id != DEV_TREZOR1: continue if debug: if not is_debuglink(dev): @@ -145,15 +146,11 @@ def enumerate(cls, debug: bool = False) -> Iterable["HidTransport"]: return devices def find_debug(self) -> "HidTransport": - if self.protocol.VERSION >= 2: - # use the same device - return self - else: - # For v1 protocol, find debug USB interface for the same serial number - for debug in HidTransport.enumerate(debug=True): - if debug.device["serial_number"] == self.device["serial_number"]: - return debug - raise TransportException("Debug HID device not found") + # For v1 protocol, find debug USB interface for the same serial number + for debug in HidTransport.enumerate(debug=True): + if debug.device["serial_number"] == self.device["serial_number"]: + return debug + raise TransportException("Debug HID device not found") def is_wirelink(dev: HidDevice) -> bool: diff --git a/hwilib/devices/trezorlib/transport/protocol.py b/hwilib/devices/trezorlib/transport/protocol.py index 00a066f88..da3806cd7 100644 --- a/hwilib/devices/trezorlib/transport/protocol.py +++ b/hwilib/devices/trezorlib/transport/protocol.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -15,15 +15,12 @@ # If not, see . import logging -import os import struct -from io import BytesIO from typing import Tuple from typing_extensions import Protocol as StructuralType -from . import Transport -from .. import mapping, protobuf +from . import MessagePayload, Transport REPLEN = 64 @@ -71,7 +68,6 @@ class Protocol: - open and close physical connections, - and send and receive binary chunks. - We declare a protocol version (we have implementations of v1 and v2). For now, the class also handles session counting and opening the underlying Handle. This will probably be removed in the future. @@ -79,8 +75,6 @@ class Protocol: its messages. """ - VERSION = None # type: int - def __init__(self, handle: Handle) -> None: self.handle = handle self.session_counter = 0 @@ -92,14 +86,14 @@ def begin_session(self) -> None: self.session_counter += 1 def end_session(self) -> None: - if self.session_counter == 1: + self.session_counter = max(self.session_counter - 1, 0) + if self.session_counter == 0: self.handle.close() - self.session_counter -= 1 - def read(self) -> protobuf.MessageType: + def read(self) -> MessagePayload: raise NotImplementedError - def write(self, message: protobuf.MessageType) -> None: + def write(self, message_type: int, message_data: bytes) -> None: raise NotImplementedError @@ -113,10 +107,10 @@ class ProtocolBasedTransport(Transport): def __init__(self, protocol: Protocol) -> None: self.protocol = protocol - def write(self, message: protobuf.MessageType) -> None: - self.protocol.write(message) + def write(self, message_type: int, message_data: bytes) -> None: + self.protocol.write(message_type, message_data) - def read(self) -> protobuf.MessageType: + def read(self) -> MessagePayload: return self.protocol.read() def begin_session(self) -> None: @@ -131,18 +125,11 @@ class ProtocolV1(Protocol): Does not understand sessions. """ - VERSION = 1 + HEADER_LEN = struct.calcsize(">HL") - def write(self, msg: protobuf.MessageType) -> None: - LOG.debug( - "sending message: {}".format(msg.__class__.__name__), - extra={"protobuf": msg}, - ) - data = BytesIO() - protobuf.dump_message(data, msg) - ser = data.getvalue() - header = struct.pack(">HL", mapping.get_type(msg), len(ser)) - buffer = bytearray(b"##" + header + ser) + def write(self, message_type: int, message_data: bytes) -> None: + header = struct.pack(">HL", message_type, len(message_data)) + buffer = bytearray(b"##" + header + message_data) while buffer: # Report ID, data padded to 63 bytes @@ -151,7 +138,7 @@ def write(self, msg: protobuf.MessageType) -> None: self.handle.write_chunk(chunk) buffer = buffer[63:] - def read(self) -> protobuf.MessageType: + def read(self) -> MessagePayload: buffer = bytearray() # Read header with first part of message data msg_type, datalen, first_chunk = self.read_first() @@ -161,28 +148,18 @@ def read(self) -> protobuf.MessageType: while len(buffer) < datalen: buffer.extend(self.read_next()) - # Strip padding - data = BytesIO(buffer[:datalen]) - - # Parse to protobuf - msg = protobuf.load_message(data, mapping.get_class(msg_type)) - LOG.debug( - "received message: {}".format(msg.__class__.__name__), - extra={"protobuf": msg}, - ) - return msg + return msg_type, buffer[:datalen] def read_first(self) -> Tuple[int, int, bytes]: chunk = self.handle.read_chunk() if chunk[:3] != b"?##": raise RuntimeError("Unexpected magic characters") try: - headerlen = struct.calcsize(">HL") - msg_type, datalen = struct.unpack(">HL", chunk[3 : 3 + headerlen]) + msg_type, datalen = struct.unpack(">HL", chunk[3 : 3 + self.HEADER_LEN]) except Exception: raise RuntimeError("Cannot parse header") - data = chunk[3 + headerlen :] + data = chunk[3 + self.HEADER_LEN :] return msg_type, datalen, data def read_next(self) -> bytes: @@ -190,17 +167,3 @@ def read_next(self) -> bytes: if chunk[:1] != b"?": raise RuntimeError("Unexpected magic characters") return chunk[1:] - - -def get_protocol(handle: Handle, want_v2: bool) -> Protocol: - """Make a Protocol instance for the given handle. - - Each transport can have a preference for using a particular protocol version. - This preference is overridable through `TREZOR_PROTOCOL_V1` environment variable, - which forces the library to use V1 anyways. - - As of 11/2018, no devices support V2, so we enforce V1 here. It is still possible - to set `TREZOR_PROTOCOL_V1=0` and thus enable V2 protocol for transports that ask - for it (i.e., USB transports for Trezor T). - """ - return ProtocolV1(handle) diff --git a/hwilib/devices/trezorlib/transport/udp.py b/hwilib/devices/trezorlib/transport/udp.py index 1195614f2..447ae2498 100644 --- a/hwilib/devices/trezorlib/transport/udp.py +++ b/hwilib/devices/trezorlib/transport/udp.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -14,11 +14,18 @@ # You should have received a copy of the License along with this library. # If not, see . +import logging import socket +import time from typing import Iterable, Optional, cast +from ..log import DUMP_PACKETS from . import TransportException -from .protocol import ProtocolBasedTransport, get_protocol +from .protocol import ProtocolBasedTransport, ProtocolV1 + +SOCKET_TIMEOUT = 10 + +LOG = logging.getLogger(__name__) class UdpTransport(ProtocolBasedTransport): @@ -37,10 +44,9 @@ def __init__(self, device: str = None) -> None: host = devparts[0] port = int(devparts[1]) if len(devparts) > 1 else UdpTransport.DEFAULT_PORT self.device = (host, port) - self.socket = None # type: Optional[socket.socket] + self.socket: Optional[socket.socket] = None - protocol = get_protocol(self, want_v2=False) - super().__init__(protocol=protocol) + super().__init__(protocol=ProtocolV1(self)) def get_path(self) -> str: return "{}:{}:{}".format(self.PATH_PREFIX, *self.device) @@ -58,7 +64,7 @@ def _try_path(cls, path: str) -> "UdpTransport": return d else: raise TransportException( - "No TREZOR device found at address {}".format(path) + "No Trezor device found at address {}".format(d.get_path()) ) finally: d.close() @@ -82,10 +88,26 @@ def find_by_path(cls, path: str, prefix_search: bool = False) -> "UdpTransport": path = path.replace("{}:".format(cls.PATH_PREFIX), "") return cls._try_path(path) + def wait_until_ready(self, timeout: float = 10) -> None: + try: + self.open() + self.socket.settimeout(0) + start = time.monotonic() + while True: + if self._ping(): + break + elapsed = time.monotonic() - start + if elapsed >= timeout: + raise TransportException("Timed out waiting for connection.") + + time.sleep(0.05) + finally: + self.close() + def open(self) -> None: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.connect(self.device) - self.socket.settimeout(1) + self.socket.settimeout(SOCKET_TIMEOUT) def close(self) -> None: if self.socket is not None: @@ -107,6 +129,7 @@ def write_chunk(self, chunk: bytes) -> None: assert self.socket is not None if len(chunk) != 64: raise TransportException("Unexpected data length") + LOG.log(DUMP_PACKETS, "sending packet: {}".format(chunk.hex())) self.socket.sendall(chunk) def read_chunk(self) -> bytes: @@ -117,6 +140,7 @@ def read_chunk(self) -> bytes: break except socket.timeout: continue + LOG.log(DUMP_PACKETS, "received packet: {}".format(chunk.hex())) if len(chunk) != 64: raise TransportException("Unexpected chunk size: %d" % len(chunk)) return bytearray(chunk) diff --git a/hwilib/devices/trezorlib/transport/webusb.py b/hwilib/devices/trezorlib/transport/webusb.py index 5a718fb18..ba6db9dc3 100644 --- a/hwilib/devices/trezorlib/transport/webusb.py +++ b/hwilib/devices/trezorlib/transport/webusb.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -20,6 +20,7 @@ import time from typing import Iterable, Optional +from ..log import DUMP_PACKETS from . import TREZORS, UDEV_RULES_STR, TransportException from .protocol import ProtocolBasedTransport, ProtocolV1 @@ -43,7 +44,7 @@ def __init__(self, device: "usb1.USBDevice", debug: bool = False) -> None: self.interface = DEBUG_INTERFACE if debug else INTERFACE self.endpoint = DEBUG_ENDPOINT if debug else ENDPOINT self.count = 0 - self.handle = None # type: Optional[usb1.USBDeviceHandle] + self.handle: Optional[usb1.USBDeviceHandle] = None def open(self) -> None: self.handle = self.device.open() @@ -65,6 +66,7 @@ def write_chunk(self, chunk: bytes) -> None: assert self.handle is not None if len(chunk) != 64: raise TransportException("Unexpected chunk size: %d" % len(chunk)) + LOG.log(DUMP_PACKETS, "writing packet: {}".format(chunk.hex())) self.handle.interruptWrite(self.endpoint, chunk) def read_chunk(self) -> bytes: @@ -76,6 +78,7 @@ def read_chunk(self) -> bytes: break else: time.sleep(0.001) + LOG.log(DUMP_PACKETS, "read packet: {}".format(chunk.hex())) if len(chunk) != 64: raise TransportException("Unexpected chunk size: %d" % len(chunk)) return chunk @@ -105,11 +108,8 @@ def __init__( def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, dev_to_str(self.device)) - def get_usb_vendor_id(self) -> int: - return self.device.getVendorID() - @classmethod - def enumerate(cls) -> Iterable["WebUsbTransport"]: + def enumerate(cls, usb_reset=False) -> Iterable["WebUsbTransport"]: if cls.context is None: cls.context = usb1.USBContext() cls.context.open() @@ -129,19 +129,18 @@ def enumerate(cls) -> Iterable["WebUsbTransport"]: # non-functional. dev.getProduct() devices.append(WebUsbTransport(dev)) - except Exception: + except usb1.USBErrorNotSupported: pass + except usb1.USBErrorPipe: + if usb_reset: + handle = dev.open() + handle.resetDevice() + handle.close() return devices def find_debug(self) -> "WebUsbTransport": - if self.protocol.VERSION >= 2: - # TODO test this - # XXX this is broken right now because sessions don't really work - # For v2 protocol, use the same WebUSB interface with a different session - return WebUsbTransport(self.device, self.handle) - else: - # For v1 protocol, find debug USB interface for the same serial number - return WebUsbTransport(self.device, debug=True) + # For v1 protocol, find debug USB interface for the same serial number + return WebUsbTransport(self.device, debug=True) def is_vendor_class(dev: "usb1.USBDevice") -> bool: diff --git a/hwilib/devices/trezorlib/ui.py b/hwilib/devices/trezorlib/ui.py deleted file mode 100644 index dafcae146..000000000 --- a/hwilib/devices/trezorlib/ui.py +++ /dev/null @@ -1,111 +0,0 @@ -# This file is part of the Trezor project. -# -# Copyright (C) 2012-2018 SatoshiLabs and contributors -# -# This library is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 -# as published by the Free Software Foundation. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the License along with this library. -# If not, see . - -import os -import sys - -from mnemonic import Mnemonic - -from . import device -from .exceptions import Cancelled -from .messages import PinMatrixRequestType, WordRequestType - -PIN_MATRIX_DESCRIPTION = """ -Use the numeric keypad to describe number positions. The layout is: - 7 8 9 - 4 5 6 - 1 2 3 -""".strip() - -RECOVERY_MATRIX_DESCRIPTION = """ -Use the numeric keypad to describe positions. -For the word list use only left and right keys. -Use backspace to correct an entry. - -The keypad layout is: - 7 8 9 7 | 9 - 4 5 6 4 | 6 - 1 2 3 1 | 3 -""".strip() - -PIN_GENERIC = None -PIN_CURRENT = PinMatrixRequestType.Current -PIN_NEW = PinMatrixRequestType.NewFirst -PIN_CONFIRM = PinMatrixRequestType.NewSecond - - -def echo(msg): - print(msg, file=sys.stderr) - -def prompt(msg, hide_input=False): - if hide_input: - import getpass - return getpass.getpass(msg + ' :\n') - else: - return input(msg + ':\n') - -class PassphraseUI: - def __init__(self, passphrase): - self.passphrase = passphrase - self.pinmatrix_shown = False - self.prompt_shown = False - self.always_prompt = False - self.return_passphrase = True - - def button_request(self, code): - if not self.prompt_shown: - echo("Please confirm action on your Trezor device") - if not self.always_prompt: - self.prompt_shown = True - - def get_pin(self, code=None): - raise NotImplementedError('get_pin is not needed') - - def disallow_passphrase(self): - self.return_passphrase = False - - def get_passphrase(self): - if self.return_passphrase: - return self.passphrase - raise ValueError('Passphrase from Host is not allowed for Trezor T') - -def mnemonic_words(expand=False, language="english"): - if expand: - wordlist = Mnemonic(language).wordlist - else: - wordlist = set() - - def expand_word(word): - if not expand: - return word - if word in wordlist: - return word - matches = [w for w in wordlist if w.startswith(word)] - if len(matches) == 1: - return word - echo("Choose one of: " + ", ".join(matches)) - raise KeyError(word) - - def get_word(type): - assert type == WordRequestType.Plain - while True: - try: - word = prompt("Enter one word of mnemonic") - return expand_word(word) - except KeyError: - pass - - return get_word From 4f480e49ffb772b585aba96ba310687cb8f2f91d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 2 Feb 2021 18:22:13 -0500 Subject: [PATCH 272/634] Modifications to trezorlib to work with our usage --- hwilib/devices/trezorlib/btc.py | 2 ++ hwilib/devices/trezorlib/client.py | 1 - hwilib/devices/trezorlib/transport/hid.py | 6 +++--- hwilib/devices/trezorlib/transport/webusb.py | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index 775dcf767..2a691b6cd 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -279,6 +279,8 @@ def copy_tx_meta(tx: messages.TransactionType) -> messages.TransactionType: # Device asked for one more information, let's process it. if res.details.tx_hash is not None: + if res.details.tx_hash not in prev_txes: + raise ValueError(f"Previous transaction {res.details.tx_hash.hex()} not available") current_tx = prev_txes[res.details.tx_hash] else: current_tx = this_tx diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index a75be139a..1a4242ff4 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -93,7 +93,6 @@ def __init__( self.ui = ui self.session_counter = 0 self.session_id = session_id - self.init_device(session_id=session_id) def open(self): if self.session_counter == 0: diff --git a/hwilib/devices/trezorlib/transport/hid.py b/hwilib/devices/trezorlib/transport/hid.py index 4dd120a50..29bb61e06 100644 --- a/hwilib/devices/trezorlib/transport/hid.py +++ b/hwilib/devices/trezorlib/transport/hid.py @@ -17,7 +17,7 @@ import logging import sys import time -from typing import Any, Dict, Iterable +from typing import Any, Dict, Iterable, Optional, Set, Tuple from ..log import DUMP_PACKETS from . import DEV_TREZOR1, UDEV_RULES_STR, TransportException @@ -130,11 +130,11 @@ def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, self.device["path"].decode()) @classmethod - def enumerate(cls, debug: bool = False) -> Iterable["HidTransport"]: + def enumerate(cls, debug: bool = False, usb_ids: Set[Tuple[int, int]] = {DEV_TREZOR1}) -> Iterable["HidTransport"]: devices = [] for dev in hid.enumerate(0, 0): usb_id = (dev["vendor_id"], dev["product_id"]) - if usb_id != DEV_TREZOR1: + if usb_id not in usb_ids: continue if debug: if not is_debuglink(dev): diff --git a/hwilib/devices/trezorlib/transport/webusb.py b/hwilib/devices/trezorlib/transport/webusb.py index ba6db9dc3..af7a914ef 100644 --- a/hwilib/devices/trezorlib/transport/webusb.py +++ b/hwilib/devices/trezorlib/transport/webusb.py @@ -18,7 +18,7 @@ import logging import sys import time -from typing import Iterable, Optional +from typing import Iterable, Optional, Set, Tuple from ..log import DUMP_PACKETS from . import TREZORS, UDEV_RULES_STR, TransportException @@ -109,7 +109,7 @@ def get_path(self) -> str: return "%s:%s" % (self.PATH_PREFIX, dev_to_str(self.device)) @classmethod - def enumerate(cls, usb_reset=False) -> Iterable["WebUsbTransport"]: + def enumerate(cls, usb_reset=False, usb_ids: Set[Tuple[int, int]] = TREZORS) -> Iterable["WebUsbTransport"]: if cls.context is None: cls.context = usb1.USBContext() cls.context.open() @@ -117,7 +117,7 @@ def enumerate(cls, usb_reset=False) -> Iterable["WebUsbTransport"]: devices = [] for dev in cls.context.getDeviceIterator(skip_on_error=True): usb_id = (dev.getVendorID(), dev.getProductID()) - if usb_id not in TREZORS: + if usb_id not in usb_ids: continue if not is_vendor_class(dev): continue From df6e0a9c10a639bd2ded94cf25020d05ac945d75 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 2 Feb 2021 16:23:14 -0500 Subject: [PATCH 273/634] Change trezorlib messages import to messages from proto Doesn't really make sense to keep this as proto. --- hwilib/devices/trezor.py | 71 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index e3fb98469..9f68ce7f5 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -40,7 +40,7 @@ btc, device, ) -from .trezorlib import messages as proto +from .trezorlib import messages from ..base58 import ( get_xpub_fingerprint, to_address, @@ -88,8 +88,8 @@ def parse_multisig(script): key = script[offset:offset + 33] offset += 33 - hd_node = proto.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=key) - pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=[])) + hd_node = messages.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=key) + pubkeys.append(messages.HDNodePathType(node=hd_node, address_n=[])) # Check things at the end n = script[offset] - 80 @@ -101,9 +101,10 @@ def parse_multisig(script): return (False, None) # Build MultisigRedeemScriptType and return it - multisig = proto.MultisigRedeemScriptType(m=m, signatures=[b''] * n, pubkeys=pubkeys) + multisig = messages.MultisigRedeemScriptType(m=m, signatures=[b''] * n, pubkeys=pubkeys) return (True, multisig) + def trezor_exception(f): def func(*args, **kwargs): try: @@ -203,7 +204,7 @@ def sign_tx(self, tx): inputs = [] to_ignore = [] # Note down which inputs whose signatures we're going to ignore for input_num, (psbt_in, txin) in py_enumerate(list(zip(tx.inputs, tx.tx.vin))): - txinputtype = proto.TxInputType() + txinputtype = messages.TxInputType() # Set the input stuff txinputtype.prev_hash = ser_uint256(txin.prevout.hash)[::-1] @@ -237,11 +238,11 @@ def sign_tx(self, tx): if is_wit: if p2sh: - txinputtype.script_type = proto.InputScriptType.SPENDP2SHWITNESS + txinputtype.script_type = messages.InputScriptType.SPENDP2SHWITNESS else: - txinputtype.script_type = proto.InputScriptType.SPENDWITNESS + txinputtype.script_type = messages.InputScriptType.SPENDWITNESS else: - txinputtype.script_type = proto.InputScriptType.SPENDADDRESS + txinputtype.script_type = messages.InputScriptType.SPENDADDRESS txinputtype.amount = utxo.nValue # Check if P2WSH @@ -256,7 +257,7 @@ def sign_tx(self, tx): def ignore_input(): txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0), 0x80000000, 0, 0] txinputtype.multisig = None - txinputtype.script_type = proto.InputScriptType.SPENDWITNESS + txinputtype.script_type = messages.InputScriptType.SPENDWITNESS inputs.append(txinputtype) to_ignore.append(input_num) @@ -267,7 +268,7 @@ def ignore_input(): txinputtype.multisig = multisig if not is_wit: if utxo.is_p2sh: - txinputtype.script_type = proto.InputScriptType.SPENDMULTISIG + txinputtype.script_type = messages.InputScriptType.SPENDMULTISIG else: # Cannot sign bare multisig, ignore it ignore_input() @@ -324,15 +325,15 @@ def ignore_input(): # prepare outputs outputs = [] for i, out in py_enumerate(tx.tx.vout): - txoutput = proto.TxOutputType() + txoutput = messages.TxOutputType() txoutput.amount = out.nValue - txoutput.script_type = proto.OutputScriptType.PAYTOADDRESS + txoutput.script_type = messages.OutputScriptType.PAYTOADDRESS if out.is_p2pkh(): txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version) elif out.is_p2sh(): txoutput.address = to_address(out.scriptPubKey[2:22], p2sh_version) elif out.is_opreturn(): - txoutput.script_type = proto.OutputScriptType.PAYTOOPRETURN + txoutput.script_type = messages.OutputScriptType.PAYTOOPRETURN txoutput.op_return_data = out.scriptPubKey[2:] else: wit, ver, prog = out.is_witness() @@ -351,13 +352,13 @@ def ignore_input(): txoutput.address_n = keypath.path txoutput.address = None elif wit: - txoutput.script_type = proto.OutputScriptType.PAYTOWITNESS + txoutput.script_type = messages.OutputScriptType.PAYTOWITNESS txoutput.address_n = keypath.path txoutput.address = None elif out.is_p2sh() and psbt_out.redeem_script: wit, ver, prog = CTxOut(0, psbt_out.redeem_script).is_witness() if wit and len(prog) == 20: - txoutput.script_type = proto.OutputScriptType.PAYTOP2SHWITNESS + txoutput.script_type = messages.OutputScriptType.PAYTOP2SHWITNESS txoutput.address_n = keypath.path txoutput.address = None @@ -370,12 +371,12 @@ def ignore_input(): if psbt_in.non_witness_utxo: prev = psbt_in.non_witness_utxo - t = proto.TransactionType() + t = messages.TransactionType() t.version = prev.nVersion t.lock_time = prev.nLockTime for vin in prev.vin: - i = proto.TxInputType() + i = messages.TxInputType() i.prev_hash = ser_uint256(vin.prevout.hash)[::-1] i.prev_index = vin.prevout.n i.script_sig = vin.scriptSig @@ -383,7 +384,7 @@ def ignore_input(): t.inputs.append(i) for vout in prev.vout: - o = proto.TxOutputBinType() + o = messages.TxOutputBinType() o.amount = vout.nValue o.script_pubkey = vout.scriptPubKey t.bin_outputs.append(o) @@ -391,7 +392,7 @@ def ignore_input(): prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t # Sign the transaction - tx_details = proto.SignTx() + tx_details = messages.SignTx() tx_details.version = tx.tx.nVersion tx_details.lock_time = tx.tx.nLockTime signed_tx = btc.sign_tx(self.client, self.coin_name, inputs, outputs, tx_details, prevtxs) @@ -428,11 +429,11 @@ def display_singlesig_address( # Script type if addr_type == AddressType.SH_WPKH: - script_type = proto.InputScriptType.SPENDP2SHWITNESS + script_type = messages.InputScriptType.SPENDP2SHWITNESS elif addr_type == AddressType.WPKH: - script_type = proto.InputScriptType.SPENDWITNESS + script_type = messages.InputScriptType.SPENDWITNESS else: - script_type = proto.InputScriptType.SPENDADDRESS + script_type = messages.InputScriptType.SPENDADDRESS expanded_path = parse_path(keypath) @@ -464,21 +465,21 @@ def display_multisig_address( for p in pubkeys: if p.extkey is not None: xpub = p.extkey - hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) - pubkey_objs.append(proto.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) + hd_node = messages.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) + pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) else: - hd_node = proto.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=p.get_pubkey_bytes(0)) - pubkey_objs.append(proto.HDNodePathType(node=hd_node, address_n=[])) + hd_node = messages.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=p.get_pubkey_bytes(0)) + pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=[])) - multisig = proto.MultisigRedeemScriptType(m=threshold, signatures=[b''] * len(pubkey_objs), pubkeys=pubkey_objs) + multisig = messages.MultisigRedeemScriptType(m=threshold, signatures=[b''] * len(pubkey_objs), pubkeys=pubkey_objs) # Script type if addr_type == AddressType.SH_WPKH: - script_type = proto.InputScriptType.SPENDP2SHWITNESS + script_type = messages.InputScriptType.SPENDP2SHWITNESS elif addr_type == AddressType.WPKH: - script_type = proto.InputScriptType.SPENDWITNESS + script_type = messages.InputScriptType.SPENDWITNESS else: - script_type = proto.InputScriptType.SPENDMULTISIG + script_type = messages.InputScriptType.SPENDMULTISIG for p in pubkeys: keypath = p.origin.get_derivation_path() if p.origin is not None else "m/" @@ -551,7 +552,7 @@ def prompt_pin(self): raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - self.client.call_raw(proto.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=proto.InputScriptType.SPENDADDRESS)) + self.client.call_raw(messages.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=messages.InputScriptType.SPENDADDRESS)) return {'success': True} # Send the pin @@ -560,10 +561,10 @@ def send_pin(self, pin): self.client.open() if not pin.isdigit(): raise BadArgumentError("Non-numeric PIN provided") - resp = self.client.call_raw(proto.PinMatrixAck(pin=pin)) - if isinstance(resp, proto.Failure): - self.client.features = self.client.call_raw(proto.GetFeatures()) - if isinstance(self.client.features, proto.Features): + resp = self.client.call_raw(messages.PinMatrixAck(pin=pin)) + if isinstance(resp, messages.Failure): + self.client.features = self.client.call_raw(messages.GetFeatures()) + if isinstance(self.client.features, messages.Features): if not self.client.features.pin_protection: raise DeviceAlreadyUnlockedError('This device does not need a PIN') if self.client.features.pin_cached: From a46e953144b33aefa4ffd9aaeda2af0185a6f784 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 2 Feb 2021 18:22:47 -0500 Subject: [PATCH 274/634] Update trezor and keepkey implementations for updated trezorlib --- hwilib/devices/keepkey.py | 20 ++++- hwilib/devices/trezor.py | 158 ++++++++++++++++++++++++++++++-------- test/test_keepkey.py | 12 +-- test/test_trezor.py | 7 +- 4 files changed, 150 insertions(+), 47 deletions(-) diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 899e16d6b..f21e7b874 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -7,12 +7,21 @@ handle_errors, ) from .trezorlib.transport import ( - enumerate_devices, + hid, + udp, + webusb, ) -from .trezor import TrezorClient +from .trezor import TrezorClient, HID_IDS, WEBUSB_IDS py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that +KEEPKEY_HID_IDS = {(0x2B24, 0x0001)} +KEEPKEY_WEBUSB_IDS = {(0x2B24, 0x0002)} + +HID_IDS.update(KEEPKEY_HID_IDS) +WEBUSB_IDS.update(KEEPKEY_WEBUSB_IDS) + + class KeepkeyClient(TrezorClient): def __init__(self, path, password='', expert=False): super(KeepkeyClient, self).__init__(path, password, expert) @@ -20,7 +29,10 @@ def __init__(self, path, password='', expert=False): def enumerate(password=''): results = [] - for dev in enumerate_devices(): + devs = hid.HidTransport.enumerate(usb_ids=KEEPKEY_HID_IDS) + devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=KEEPKEY_WEBUSB_IDS)) + devs.extend(udp.UdpTransport.enumerate()) + for dev in devs: d_data = {} d_data['type'] = 'keepkey' @@ -31,7 +43,7 @@ def enumerate(password=''): with handle_errors(common_err_msgs["enumerate"], d_data): client = KeepkeyClient(d_data['path'], password) - client.client.init_device() + client.client.refresh_features() if 'keepkey' not in client.client.features.vendor: continue diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 9f68ce7f5..569517bb2 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -21,20 +21,13 @@ ) from .trezorlib.client import TrezorClient as Trezor from .trezorlib.debuglink import TrezorClientDebugLink -from .trezorlib.exceptions import Cancelled +from .trezorlib.exceptions import Cancelled, TrezorFailure from .trezorlib.transport import ( - enumerate_devices, - get_transport, -) -from .trezorlib.ui import ( - echo, - PassphraseUI, - mnemonic_words, - PIN_CURRENT, - PIN_NEW, - PIN_CONFIRM, - PIN_MATRIX_DESCRIPTION, - prompt, + DEV_TREZOR1, + TREZORS, + hid, + udp, + webusb, ) from .trezorlib import ( btc, @@ -61,15 +54,24 @@ ser_uint256, ) from .. import bech32 +from mnemonic import Mnemonic from usb1 import USBErrorNoDevice from types import MethodType import base64 +import getpass import logging import sys py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that +PIN_MATRIX_DESCRIPTION = """ +Use the numeric keypad to describe number positions. The layout is: + 7 8 9 + 4 5 6 + 1 2 3 +""".strip() + # Only handles up to 15 of 15 def parse_multisig(script): # Get m @@ -117,39 +119,112 @@ def func(*args, **kwargs): raise DeviceConnectionError('Device disconnected') return func + def interactive_get_pin(self, code=None): - if code == PIN_CURRENT: + if code == messages.PinMatrixRequestType.Currrent: desc = "current PIN" - elif code == PIN_NEW: + elif code == messages.PinMatrixRequestType.NewFirst: desc = "new PIN" - elif code == PIN_CONFIRM: + elif code == messages.PinMatrixRequestType.NewSecond: desc = "new PIN again" else: desc = "PIN" - echo(PIN_MATRIX_DESCRIPTION) + print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) while True: - pin = prompt("Please enter {}".format(desc), hide_input=True) + pin = getpass.getpass(f"Please entire {desc}:\n") if not pin.isdigit(): - echo("Non-numerical PIN provided, please try again") + print("Non-numerical PIN provided, please try again", file=sys.stderr) else: return pin + +def mnemonic_words(expand=False, language="english"): + if expand: + wordlist = Mnemonic(language).wordlist + else: + wordlist = set() + + def expand_word(word): + if not expand: + return word + if word in wordlist: + return word + matches = [w for w in wordlist if w.startswith(word)] + if len(matches) == 1: + return matches[0] + print("Choose one of: " + ", ".join(matches), file=sys.stderr) + raise KeyError(word) + + def get_word(type): + assert type == messages.WordRequestType.Plain + while True: + try: + word = input("Enter one word of mnemonic:\n") + return expand_word(word) + except KeyError: + pass + except Exception: + raise Cancelled from None + + return get_word + + +class PassphraseUI: + def __init__(self, passphrase): + self.passphrase = passphrase + self.pinmatrix_shown = False + self.prompt_shown = False + self.always_prompt = False + self.return_passphrase = True + + def button_request(self, code): + if not self.prompt_shown: + print("Please confirm action on your Trezor device", file=sys.stderr) + if not self.always_prompt: + self.prompt_shown = True + + def get_pin(self, code=None): + raise NotImplementedError('get_pin is not needed') + + def disallow_passphrase(self): + self.return_passphrase = False + + def get_passphrase(self): + if self.return_passphrase: + return self.passphrase + raise ValueError('Passphrase from Host is not allowed for Trezor T') + + +HID_IDS = {DEV_TREZOR1} +WEBUSB_IDS = TREZORS.copy() + + +def get_path_transport(path: str): + devs = hid.HidTransport.enumerate(usb_ids=HID_IDS) + devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=WEBUSB_IDS)) + devs.extend(udp.UdpTransport.enumerate()) + for dev in devs: + if path == dev.get_path(): + return dev + raise BadArgumentError(f"Could not find device by path: {path}") + + # This class extends the HardwareWalletClient for Trezor specific things class TrezorClient(HardwareWalletClient): def __init__(self, path, password='', expert=False): super(TrezorClient, self).__init__(path, password, expert) self.simulator = False + transport = get_path_transport(path) if path.startswith('udp'): logging.debug('Simulator found, using DebugLink') - transport = get_transport(path) self.client = TrezorClientDebugLink(transport=transport) self.simulator = True - self.client.set_passphrase(password) + self.client.use_passphrase(password) else: - self.client = Trezor(transport=get_transport(path), ui=PassphraseUI(password)) + self.client = Trezor(transport=transport, ui=PassphraseUI(password)) # if it wasn't able to find a client, throw an error if not self.client: @@ -158,12 +233,24 @@ def __init__(self, path, password='', expert=False): self.password = password self.type = 'Trezor' - def _check_unlocked(self): + def _prepare_device(self): self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' - self.client.init_device() - if self.client.features.model == 'T': + resp = self.client.refresh_features() + # If this is a Trezor One or Keepkey, do Initialize + if resp.model == '1' or resp.model == 'K1-14AM': + self.client.init_device() + # For the T, we need to check if a passphrase needs to be entered + elif resp.model == 'T': + try: + self.client.ensure_unlocked() + except TrezorFailure: + self.client.init_device() + + def _check_unlocked(self): + self._prepare_device() + if self.client.features.model == 'T' and isinstance(self.client.ui, PassphraseUI): self.client.ui.disallow_passphrase() - if self.client.features.pin_protection and not self.client.features.pin_cached: + if self.client.features.pin_protection and not self.client.features.unlocked: raise DeviceNotReadyError('{} is locked. Unlock by using \'promptpin\' and then \'sendpin\'.'.format(self.type)) # Must return a dict with the xpub @@ -503,7 +590,7 @@ def display_multisig_address( # Setup a new device @trezor_exception def setup_device(self, label='', passphrase=''): - self.client.init_device() + self._prepare_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) @@ -523,7 +610,7 @@ def wipe_device(self): # Restore device from mnemonic or xprv @trezor_exception def restore_device(self, label='', word_count=24): - self.client.init_device() + self._prepare_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) @@ -545,10 +632,10 @@ def close(self): def prompt_pin(self): self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' self.client.open() - self.client.init_device() + self._prepare_device() if not self.client.features.pin_protection: raise DeviceAlreadyUnlockedError('This device does not need a PIN') - if self.client.features.pin_cached: + if self.client.features.unlocked: raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) @@ -567,7 +654,7 @@ def send_pin(self, pin): if isinstance(self.client.features, messages.Features): if not self.client.features.pin_protection: raise DeviceAlreadyUnlockedError('This device does not need a PIN') - if self.client.features.pin_cached: + if self.client.features.unlocked: raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') return {'success': False} return {'success': True} @@ -587,7 +674,10 @@ def toggle_passphrase(self): def enumerate(password=''): results = [] - for dev in enumerate_devices(): + devs = hid.HidTransport.enumerate() + devs.extend(webusb.WebUsbTransport.enumerate()) + devs.extend(udp.UdpTransport.enumerate()) + for dev in devs: d_data = {} d_data['type'] = 'trezor' @@ -596,7 +686,7 @@ def enumerate(password=''): client = None with handle_errors(common_err_msgs["enumerate"], d_data): client = TrezorClient(d_data['path'], password) - client.client.init_device() + client._prepare_device() if 'trezor' not in client.client.features.vendor: continue @@ -604,7 +694,7 @@ def enumerate(password=''): if d_data['path'] == 'udp:127.0.0.1:21324': d_data['model'] += '_simulator' - d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached + d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.unlocked if client.client.features.model == '1': d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection # always need the passphrase sent for Trezor One if it has passphrase protection enabled else: diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 7683a511f..93c8ff4d8 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -13,7 +13,7 @@ from hwilib.devices.trezorlib.transport import enumerate_devices from hwilib.devices.trezorlib.transport.udp import UdpTransport -from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic, load_device_by_xprv +from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic from hwilib.devices.trezorlib import device, messages from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx @@ -137,9 +137,9 @@ def test_getxpub(self): vectors = json.load(f) for vec in vectors: with self.subTest(vector=vec): - # Setup with xprv + # Setup with mnemonic device.wipe(self.client) - load_device_by_xprv(client=self.client, xprv=vec['xprv'], pin='', passphrase_protection=False, label='test', language='english') + load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') # Test getmasterxpub gmxp_res = self.do_command(['-t', 'keepkey', '-d', 'udp:127.0.0.1:21324', 'getmasterxpub']) @@ -212,7 +212,7 @@ def test_pins(self): # Set a PIN device.wipe(self.client) load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test') - self.client.call(messages.ClearSession()) + self.client.call(messages.EndSession()) result = self.do_command(self.dev_args + ['enumerate']) for dev in result: if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': @@ -234,7 +234,7 @@ def test_pins(self): self.assertEqual(result['error'], 'Keepkey is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') # Prompt pin - self.client.call(messages.ClearSession()) + self.client.call(messages.EndSession()) result = self.do_command(self.dev_args + ['promptpin']) self.assertTrue(result['success']) @@ -279,7 +279,7 @@ def test_passphrase(self): self.assertNotEqual(dev['fingerprint'], fpr) # Clearing the session and starting a new one with a new passphrase should change the passphrase - self.client.call(messages.ClearSession()) + self.client.call(messages.EndSession()) result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate']) for dev in result: if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': diff --git a/test/test_trezor.py b/test/test_trezor.py index dfe7e3a3e..37fdc3176 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -148,7 +148,7 @@ def test_getxpub(self): vectors = json.load(f) for vec in vectors: with self.subTest(vector=vec): - # Setup with xprv + # Setup with mnemonic device.wipe(self.client) load_device_by_mnemonic(client=self.client, mnemonic=vec['mnemonic'], pin='', passphrase_protection=False, label='test', language='english') @@ -223,7 +223,8 @@ def test_pins(self): # Set a PIN device.wipe(self.client) load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test') - self.client.call(messages.ClearSession()) + self.client.lock(_refresh_features=False) + self.client.end_session() result = self.do_command(self.dev_args + ['enumerate']) for dev in result: if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': @@ -245,7 +246,7 @@ def test_pins(self): self.assertEqual(result['error'], 'Trezor is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') # Prompt pin - self.client.call(messages.ClearSession()) + self.client.call(messages.EndSession()) result = self.do_command(self.dev_args + ['promptpin']) self.assertTrue(result['success']) From 0de1b627b3e4a7b6d9c85e3b49eea5c2d5b28541 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 3 Feb 2021 16:49:31 -0500 Subject: [PATCH 275/634] Changes to allow trezorlib to talk to keepkey --- hwilib/devices/trezorlib/client.py | 9 ++++++--- hwilib/devices/trezorlib/debuglink.py | 3 ++- hwilib/devices/trezorlib/mapping.py | 8 ++++---- hwilib/devices/trezorlib/protobuf.py | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index 1a4242ff4..0521e7cdd 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -93,6 +93,9 @@ def __init__( self.ui = ui self.session_counter = 0 self.session_id = session_id + self.map_type_to_class_override = {} + self.vendors = VENDORS + self.minimum_versions = MINIMUM_FIRMWARE_VERSION def open(self): if self.session_counter == 0: @@ -137,7 +140,7 @@ def _raw_read(self): msg_type, len(msg_bytes), msg_bytes.hex() ), ) - msg = mapping.decode(msg_type, msg_bytes) + msg = mapping.decode(msg_type, msg_bytes, self.map_type_to_class_override) LOG.debug( "received message: {}".format(msg.__class__.__name__), extra={"protobuf": msg}, @@ -228,7 +231,7 @@ def call(self, msg): def _refresh_features(self, features: messages.Features) -> None: """Update internal fields based on passed-in Features message.""" - if features.vendor not in VENDORS: + if features.vendor not in self.vendors: raise RuntimeError("Unsupported device") self.features = features @@ -316,7 +319,7 @@ def is_outdated(self): if self.features.bootloader_mode: return False model = self.features.model or "1" - required_version = MINIMUM_FIRMWARE_VERSION[model] + required_version = self.minimum_versions[model] return self.version < required_version def check_firmware_version(self, warn_only=False): diff --git a/hwilib/devices/trezorlib/debuglink.py b/hwilib/devices/trezorlib/debuglink.py index c64ba909a..c85dbe7fd 100644 --- a/hwilib/devices/trezorlib/debuglink.py +++ b/hwilib/devices/trezorlib/debuglink.py @@ -42,6 +42,7 @@ class DebugLink: def __init__(self, transport, auto_interact=True): self.transport = transport self.allow_interactions = auto_interact + self.map_type_to_class_override = {} def open(self): self.transport.begin_session() @@ -72,7 +73,7 @@ def _call(self, msg, nowait=False): msg_type, len(msg_bytes), msg_bytes.hex() ), ) - msg = mapping.decode(ret_type, ret_bytes) + msg = mapping.decode(ret_type, ret_bytes, self.map_type_to_class_override) LOG.debug( "received message: {}".format(msg.__class__.__name__), extra={"protobuf": msg}, diff --git a/hwilib/devices/trezorlib/mapping.py b/hwilib/devices/trezorlib/mapping.py index a4e160d21..bad492df3 100644 --- a/hwilib/devices/trezorlib/mapping.py +++ b/hwilib/devices/trezorlib/mapping.py @@ -62,8 +62,8 @@ def get_type(msg): return map_class_to_type[msg.__class__] -def get_class(t): - return map_type_to_class[t] +def get_class(t, map_type_to_class_override): + return map_type_to_class_override.get(t, map_type_to_class[t]) def encode(msg: protobuf.MessageType) -> Tuple[int, bytes]: @@ -73,8 +73,8 @@ def encode(msg: protobuf.MessageType) -> Tuple[int, bytes]: return message_type, buf.getvalue() -def decode(message_type: int, message_bytes: bytes) -> protobuf.MessageType: - cls = get_class(message_type) +def decode(message_type: int, message_bytes: bytes, map_type_to_class_override={}) -> protobuf.MessageType: + cls = get_class(message_type, map_type_to_class_override) buf = io.BytesIO(message_bytes) return protobuf.load_message(buf, cls) diff --git a/hwilib/devices/trezorlib/protobuf.py b/hwilib/devices/trezorlib/protobuf.py index 0df86fe80..a90009cf5 100644 --- a/hwilib/devices/trezorlib/protobuf.py +++ b/hwilib/devices/trezorlib/protobuf.py @@ -202,8 +202,8 @@ class UnicodeType: class _MessageTypeMeta(type): def __init__(cls, name, bases, d) -> None: super().__init__(name, bases, d) - if name != "MessageType": - cls.__init__ = MessageType.__init__ + #if name != "MessageType": + # cls.__init__ = MessageType.__init__ class MessageType(metaclass=_MessageTypeMeta): From d2ce78327d3f2ed43c3f40f9263bc5586b243e1d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 3 Feb 2021 16:50:39 -0500 Subject: [PATCH 276/634] Update keepkey and trezor impls to work with trezorlib mod --- hwilib/devices/keepkey.py | 122 +++++++++++++++++++++++++++++++++++++- hwilib/devices/trezor.py | 50 +++++++++------- test/test_keepkey.py | 26 ++++---- test/test_trezor.py | 7 +-- 4 files changed, 165 insertions(+), 40 deletions(-) diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index f21e7b874..4db1390ce 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -6,12 +6,21 @@ common_err_msgs, handle_errors, ) +from .trezorlib import protobuf as p from .trezorlib.transport import ( hid, udp, webusb, ) from .trezor import TrezorClient, HID_IDS, WEBUSB_IDS +from .trezorlib.messages import ( + DebugLinkState, + Features, + HDNodeType, + ResetDevice, +) + +from typing import Dict py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that @@ -22,10 +31,116 @@ WEBUSB_IDS.update(KEEPKEY_WEBUSB_IDS) +class KeepkeyFeatures(Features): + def __init__( + self, + *, + firmware_variant: str = None, + firmware_hash: bytes = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.firmware_variant = firmware_variant + self.firmware_hash = firmware_hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('vendor', p.UnicodeType, None), + 2: ('major_version', p.UVarintType, None), + 3: ('minor_version', p.UVarintType, None), + 4: ('patch_version', p.UVarintType, None), + 5: ('bootloader_mode', p.BoolType, None), + 6: ('device_id', p.UnicodeType, None), + 7: ('pin_protection', p.BoolType, None), + 8: ('passphrase_protection', p.BoolType, None), + 9: ('language', p.UnicodeType, None), + 10: ('label', p.UnicodeType, None), + 12: ('initialized', p.BoolType, None), + 13: ('revision', p.BytesType, None), + 14: ('bootloader_hash', p.BytesType, None), + 15: ('imported', p.BoolType, None), + 16: ('unlocked', p.BoolType, None), + 21: ('model', p.UnicodeType, None), + 22: ('firmware_variant', p.UnicodeType, None), + 23: ('firmware_hash', p.BytesType, None), + 24: ('no_backup', p.BoolType, None), + 25: ('wipe_code_protection', p.BoolType, None), + } + + +class KeepkeyResetDevice(ResetDevice): + def __init__( + self, + *, + auto_lock_delay_ms: int = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.auto_lock_delay_ms = auto_lock_delay_ms + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('display_random', p.BoolType, None), + 2: ('strength', p.UVarintType, 256), # default=256 + 3: ('passphrase_protection', p.BoolType, None), + 4: ('pin_protection', p.BoolType, None), + 5: ('language', p.UnicodeType, "en-US"), # default=en-US + 6: ('label', p.UnicodeType, None), + 7: ('no_backup', p.BoolType, None), + 8: ('auto_lock_delay_ms', p.UVarintType, None), + 9: ('u2f_counter', p.UVarintType, None), + } + + +class KeepkeyDebugLinkState(DebugLinkState): + def __init__( + self, + *, + recovery_cipher: str = None, + recovery_auto_completed_word: str = None, + firmware_hash: bytes = None, + storage_hash: bytes = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.recovery_cipher = recovery_cipher + self.recovery_auto_completed_word = recovery_auto_completed_word + self.firmware_hash = firmware_hash + self.storage_hash = storage_hash + + @classmethod + def get_fields(cls) -> Dict: + return { + 1: ('layout', p.BytesType, None), + 2: ('pin', p.UnicodeType, None), + 3: ('matrix', p.UnicodeType, None), + 4: ('mnemonic_secret', p.BytesType, None), + 5: ('node', HDNodeType, None), + 6: ('passphrase_protection', p.BoolType, None), + 7: ('reset_word', p.UnicodeType, None), + 8: ('reset_entropy', p.BytesType, None), + 9: ('recovery_fake_word', p.UnicodeType, None), + 10: ('recovery_word_pos', p.UVarintType, None), + 11: ('recovery_cipher', p.UnicodeType, None), + 12: ('recovery_auto_completed_word', p.UnicodeType, None), + 13: ('firmware_hash', p.BytesType, None), + 14: ('storage_hash', p.BytesType, None), + } + + class KeepkeyClient(TrezorClient): def __init__(self, path, password='', expert=False): super(KeepkeyClient, self).__init__(path, password, expert) self.type = 'Keepkey' + self.client.vendors = ("keepkey.com") + self.client.minimum_versions = {"K1-14AM": (0, 0, 0)} + self.client.map_type_to_class_override[KeepkeyFeatures.MESSAGE_WIRE_TYPE] = KeepkeyFeatures + self.client.map_type_to_class_override[KeepkeyResetDevice.MESSAGE_WIRE_TYPE] = KeepkeyResetDevice + if self.simulator: + self.client.debug.map_type_to_class_override[KeepkeyDebugLinkState.MESSAGE_WIRE_TYPE] = KeepkeyDebugLinkState + def enumerate(password=''): results = [] @@ -43,14 +158,17 @@ def enumerate(password=''): with handle_errors(common_err_msgs["enumerate"], d_data): client = KeepkeyClient(d_data['path'], password) - client.client.refresh_features() + try: + client.client.refresh_features() + except TypeError: + continue if 'keepkey' not in client.client.features.vendor: continue if d_data['path'] == 'udp:127.0.0.1:21324': d_data['model'] += '_simulator' - d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached + d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.unlocked d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection # always need the passphrase sent for Keepkey if it has passphrase protection enabled if d_data['needs_pin_sent']: raise DeviceNotReadyError('Keepkey is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 569517bb2..05d3002f6 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -291,12 +291,11 @@ def sign_tx(self, tx): inputs = [] to_ignore = [] # Note down which inputs whose signatures we're going to ignore for input_num, (psbt_in, txin) in py_enumerate(list(zip(tx.inputs, tx.tx.vin))): - txinputtype = messages.TxInputType() - - # Set the input stuff - txinputtype.prev_hash = ser_uint256(txin.prevout.hash)[::-1] - txinputtype.prev_index = txin.prevout.n - txinputtype.sequence = txin.nSequence + txinputtype = messages.TxInputType( + prev_hash=ser_uint256(txin.prevout.hash)[::-1], + prev_index=txin.prevout.n, + sequence=txin.nSequence, + ) # Detrermine spend type scriptcode = b'' @@ -412,8 +411,7 @@ def ignore_input(): # prepare outputs outputs = [] for i, out in py_enumerate(tx.tx.vout): - txoutput = messages.TxOutputType() - txoutput.amount = out.nValue + txoutput = messages.TxOutputType(amount=out.nValue) txoutput.script_type = messages.OutputScriptType.PAYTOADDRESS if out.is_p2pkh(): txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version) @@ -463,26 +461,33 @@ def ignore_input(): t.lock_time = prev.nLockTime for vin in prev.vin: - i = messages.TxInputType() - i.prev_hash = ser_uint256(vin.prevout.hash)[::-1] - i.prev_index = vin.prevout.n - i.script_sig = vin.scriptSig - i.sequence = vin.nSequence + i = messages.TxInputType( + prev_hash=ser_uint256(vin.prevout.hash)[::-1], + prev_index=vin.prevout.n, + script_sig=vin.scriptSig, + sequence=vin.nSequence, + ) t.inputs.append(i) for vout in prev.vout: - o = messages.TxOutputBinType() - o.amount = vout.nValue - o.script_pubkey = vout.scriptPubKey + o = messages.TxOutputBinType( + amount=vout.nValue, + script_pubkey=vout.scriptPubKey, + ) t.bin_outputs.append(o) logging.debug(psbt_in.non_witness_utxo.hash) prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t # Sign the transaction - tx_details = messages.SignTx() - tx_details.version = tx.tx.nVersion - tx_details.lock_time = tx.tx.nLockTime - signed_tx = btc.sign_tx(self.client, self.coin_name, inputs, outputs, tx_details, prevtxs) + signed_tx = btc.sign_tx( + client=self.client, + coin_name=self.coin_name, + inputs=inputs, + outputs=outputs, + prev_txes=prevtxs, + version=tx.tx.nVersion, + lock_time=tx.tx.nLockTime, + ) # Each input has one signature for input_num, (psbt_in, sig) in py_enumerate(list(zip(tx.inputs, signed_tx[0]))): @@ -686,7 +691,10 @@ def enumerate(password=''): client = None with handle_errors(common_err_msgs["enumerate"], d_data): client = TrezorClient(d_data['path'], password) - client._prepare_device() + try: + client._prepare_device() + except TypeError: + continue if 'trezor' not in client.client.features.vendor: continue diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 93c8ff4d8..79a56efd0 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -11,15 +11,18 @@ import time import unittest -from hwilib.devices.trezorlib.transport import enumerate_devices from hwilib.devices.trezorlib.transport.udp import UdpTransport from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic from hwilib.devices.trezorlib import device, messages from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx from hwilib.cli import process_commands -from hwilib.devices.keepkey import KeepkeyClient - +from hwilib.devices.keepkey import ( + KeepkeyClient, + KeepkeyDebugLinkState, + KeepkeyFeatures, + KeepkeyResetDevice, +) from types import MethodType def get_pin(self, code=None): @@ -57,12 +60,13 @@ def start(self): time.sleep(0.05) # Setup the emulator - for dev in enumerate_devices(): - # Find the udp transport, that's the emulator - if isinstance(dev, UdpTransport): - wirelink = dev - break + wirelink = UdpTransport.enumerate()[0] client = TrezorClientDebugLink(wirelink) + client.vendors = ("keepkey.com") + client.minimum_versions = {"K1-14AM": (0, 0, 0)} + client.map_type_to_class_override[KeepkeyFeatures.MESSAGE_WIRE_TYPE] = KeepkeyFeatures + client.map_type_to_class_override[KeepkeyResetDevice.MESSAGE_WIRE_TYPE] = KeepkeyResetDevice + client.debug.map_type_to_class_override[KeepkeyDebugLinkState.MESSAGE_WIRE_TYPE] = KeepkeyDebugLinkState client.init_device() device.wipe(client) load_device_by_mnemonic(client=client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=False, label='test') # From Trezor device tests @@ -212,7 +216,7 @@ def test_pins(self): # Set a PIN device.wipe(self.client) load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test') - self.client.call(messages.EndSession()) + self.client.call(messages.LockDevice()) result = self.do_command(self.dev_args + ['enumerate']) for dev in result: if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324': @@ -234,7 +238,7 @@ def test_pins(self): self.assertEqual(result['error'], 'Keepkey is locked. Unlock by using \'promptpin\' and then \'sendpin\'.') # Prompt pin - self.client.call(messages.EndSession()) + self.client.call(messages.LockDevice()) result = self.do_command(self.dev_args + ['promptpin']) self.assertTrue(result['success']) @@ -279,7 +283,7 @@ def test_passphrase(self): self.assertNotEqual(dev['fingerprint'], fpr) # Clearing the session and starting a new one with a new passphrase should change the passphrase - self.client.call(messages.EndSession()) + self.client.call(messages.LockDevice()) result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate']) for dev in result: if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324': diff --git a/test/test_trezor.py b/test/test_trezor.py index 37fdc3176..e2d0f2943 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -12,7 +12,6 @@ import time import unittest -from hwilib.devices.trezorlib.transport import enumerate_devices from hwilib.devices.trezorlib.transport.udp import UdpTransport from hwilib.devices.trezorlib.debuglink import TrezorClientDebugLink, load_device_by_mnemonic from hwilib.devices.trezorlib import device, messages @@ -62,11 +61,7 @@ def start(self): time.sleep(0.05) # Setup the emulator - for dev in enumerate_devices(): - # Find the udp transport, that's the emulator - if isinstance(dev, UdpTransport): - wirelink = dev - break + wirelink = UdpTransport.enumerate()[0] client = TrezorClientDebugLink(wirelink) client.init_device() device.wipe(client) From ec44f1ec506dab40ce60a6a173c8a9168c6484ad Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 3 Feb 2021 16:55:34 -0500 Subject: [PATCH 277/634] Mention commits for trezorlib mods in readme --- hwilib/devices/trezorlib/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hwilib/devices/trezorlib/README.md b/hwilib/devices/trezorlib/README.md index dd09fbffd..ff212562c 100644 --- a/hwilib/devices/trezorlib/README.md +++ b/hwilib/devices/trezorlib/README.md @@ -10,3 +10,5 @@ This stripped down version was made at commit [e4c406822c00695aaf7cd420634643236 - Removed functions that HWI does not use or plan to use - Changed `TrezorClient` from calling `init_device()` (HWI needs this behavior and doing it in the library makes this simpler) - Add Keepkey support. Some fields of some messages had to be removed to support both the Keepkey and the Trezor in the same library + +See commits 4f480e49ffb772b585aba96ba310687cb8f2f91d and 0de1b627b3e4a7b6d9c85e3b49eea5c2d5b28541 for the modifications made. From e710e4080ee7a5dec7791d6ffcbf0af35cc7b5d8 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Wed, 11 Nov 2020 01:10:13 +0100 Subject: [PATCH 278/634] bitbox02: implement display_multisig_address If the multisig setup is not yet registered on the device, the user is prompted to register and verify the setup on the device. Only p2wsh (native segwit) and p2wsh-p2sh (p2sh wrapped segwit multisig) is supported. Legacy `p2sh` multisig is not supported. --- hwilib/devices/bitbox02.py | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 293baecb0..53d63c26d 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -370,7 +370,7 @@ def _multisig_scriptconfig( threshold: int, origin_infos: Mapping[bytes, KeyOriginInfo], script_type: bitbox02.btc.BTCScriptConfig.Multisig.ScriptType, - ) -> Tuple[str, bitbox02.btc.BTCScriptConfigWithKeypath]: + ) -> Tuple[bytes, bitbox02.btc.BTCScriptConfigWithKeypath]: """ From a threshold, {xpub: KeyOriginInfo} mapping and multisig script type, return our xpub and the BitBox02 multisig script config. @@ -380,9 +380,9 @@ def _multisig_scriptconfig( our_xpub_index = None our_account_keypath = None - xpubs: List[str] = [] + xpubs: List[bytes] = [] for i, (xpub, keyinfo) in builtins.enumerate(origin_infos.items()): - xpubs.append(base58.b58encode_check(xpub).decode()) + xpubs.append(xpub) if device_fingerprint == keyinfo.fingerprint and keyinfo.path: if _xpubs_equal_ignoring_version( base58.b58decode_check(self._get_xpub(keyinfo.path)), xpub @@ -403,7 +403,7 @@ def _multisig_scriptconfig( script_config=bitbox02.btc.BTCScriptConfig( multisig=bitbox02.btc.BTCScriptConfig.Multisig( threshold=threshold, - xpubs=map(util.parse_xpub, xpubs), + xpubs=[util.parse_xpub(base58.b58encode_check(xpub).decode()) for xpub in xpubs], our_xpub_index=our_xpub_index, script_type=script_type, ) @@ -445,7 +445,41 @@ def display_multisig_address( pubkeys: List[PubkeyProvider], addr_type: AddressType, ) -> Dict[str, str]: - raise NotImplementedError("BitBox02 multisig not integrated into HWI yet") + path_suffixes = set(p.deriv_path for p in pubkeys) + if len(path_suffixes) != 1: + # Path suffix refers to the path after the account-level xpub, usually //
. + # The BitBox02 currently enforces that all of them are the same. + raise BadArgumentError("All multisig path suffixes must be the same") + + # Figure out which of the cosigners is us. + key_origin_infos = {} + keypaths = {} + for pk in pubkeys: + assert pk.extkey and pk.origin + key_origin_infos[pk.extkey.serialize()] = pk.origin + keypaths[pk.extkey.serialize()] = pk.get_full_derivation_path(0) + + if addr_type == AddressType.SH_WPKH: + script_type = bitbox02.btc.BTCScriptConfig.Multisig.P2WSH_P2SH + elif addr_type == AddressType.WPKH: + script_type = bitbox02.btc.BTCScriptConfig.Multisig.P2WSH + else: + raise BadArgumentError( + "BitBox02 currently only supports the following multisig script types: P2WSH, P2WSH_P2SH" + ) + our_xpub, script_config_with_keypath = self._multisig_scriptconfig( + threshold, key_origin_infos, script_type + ) + script_config = script_config_with_keypath.script_config + account_keypath: Sequence[int] = script_config_with_keypath.keypath + self._maybe_register_script_config(script_config, account_keypath) + keypath = parse_path(keypaths[our_xpub]) + + bb02 = self.init() + address = bb02.btc_address( + keypath, coin=self._get_coin(), script_config=script_config, display=True + ) + return {"address": address} @bitbox02_exception def sign_tx(self, psbt: PSBT) -> Dict[str, str]: From 3e737644213657fd716fa9bcca495610654a346d Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Wed, 10 Feb 2021 19:48:00 +0100 Subject: [PATCH 279/634] Introduce Chain enum; replace is_testnet by Chain where possible (except key.py) --- hwilib/cli.py | 6 ++---- hwilib/commands.py | 3 ++- hwilib/common.py | 23 +++++++++++++++++++++++ hwilib/devices/bitbox02.py | 5 +++-- hwilib/devices/coldcard.py | 3 ++- hwilib/devices/digitalbitbox.py | 4 +++- hwilib/devices/ledger.py | 3 ++- hwilib/devices/trezor.py | 11 ++++++----- hwilib/gui.py | 14 ++++++-------- hwilib/hwwclient.py | 3 ++- 10 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 hwilib/common.py diff --git a/hwilib/cli.py b/hwilib/cli.py index 131d6302c..e22520e69 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -20,6 +20,7 @@ wipe_device, install_udev_rules, ) +from .common import Chain from .errors import ( handle_errors, DEVICE_CONN_ERROR, @@ -117,14 +118,12 @@ def error(self, message): self.exit(2) def process_commands(cli_args): - CHAINS = ['main', 'test', 'regtest', 'signet'] - parser = HWIArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__)) parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to') parser.add_argument('--device-type', '-t', help='Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.') parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') parser.add_argument('--stdinpass', help='Enter the device password on the command line', action='store_true') - parser.add_argument('--chain', help='Select chain to work with ({})'.format(', '.join(CHAINS)), default='main', choices=CHAINS) + parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN) parser.add_argument('--debug', help='Print debug statements', action='store_true') parser.add_argument('--fingerprint', '-f', help='Specify the device to connect to using the first 4 bytes of the hash160 of the master public key. It will connect to the first device that matches this fingerprint.') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) @@ -268,7 +267,6 @@ def process_commands(cli_args): return {'error': 'You must specify a device type or fingerprint for all commands except enumerate', 'code': NO_DEVICE_TYPE} client.chain = args.chain - client.is_testnet = args.chain in ['test', 'regtest', 'signet'] # Do the commands with handle_errors(result=result, debug=args.debug): diff --git a/hwilib/commands.py b/hwilib/commands.py index 904f7ac7c..18ffc6e76 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -32,6 +32,7 @@ WSHDescriptor, ) from .devices import __all__ as all_devs +from .common import Chain from itertools import count @@ -147,7 +148,7 @@ def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=Addre parsed_path.append(H_(44)) # Coin type - if client.chain == 'main': + if client.chain == Chain.MAIN: parsed_path.append(H_(0)) else: parsed_path.append(H_(1)) diff --git a/hwilib/common.py b/hwilib/common.py new file mode 100644 index 000000000..a0a1f0fe3 --- /dev/null +++ b/hwilib/common.py @@ -0,0 +1,23 @@ +from enum import Enum + +from typing import Union + + +class Chain(Enum): + MAIN = 0 + TEST = 1 + REGTEST = 2 + SIGNET = 3 + + def __str__(self) -> str: + return self.name.lower() + + def __repr__(self) -> str: + return str(self) + + @staticmethod + def argparse(s: str) -> Union['Chain', str]: + try: + return Chain[s.upper()] + except KeyError: + return s diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 293baecb0..28383fdeb 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -44,6 +44,7 @@ KeyOriginInfo, parse_path, ) +from ..common import Chain import hid # type: ignore @@ -327,14 +328,14 @@ def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: ) def _get_coin(self) -> bitbox02.btc.BTCCoin: - if self.is_testnet: + if self.chain != Chain.MAIN: return bitbox02.btc.TBTC return bitbox02.btc.BTC def _get_xpub(self, keypath: Sequence[int]) -> str: xpub_type = ( bitbox02.btc.BTCPubRequest.TPUB - if self.is_testnet + if self.chain != Chain.MAIN else bitbox02.btc.BTCPubRequest.XPUB ) return self.init().btc_xpub( diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index f5270b8e2..8889a5502 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -48,6 +48,7 @@ AddressType, PSBT, ) +from ..common import Chain from hashlib import sha256 import base64 @@ -95,7 +96,7 @@ def get_pubkey_at_path(self, path): path = path.replace('h', '\'') path = path.replace('H', '\'') xpub = self.device.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) - if self.is_testnet: + if self.chain != Chain.MAIN: result = {'xpub': xpub_main_2_test(xpub)} else: result = {'xpub': xpub} diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 6d9559fda..96bef62ec 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -55,6 +55,8 @@ get_xpub_fingerprint, xpub_main_2_test, ) +from ..common import Chain + applen = 225280 # flash size minus bootloader length chunksize = 8 * 512 usb_report_size = 64 # firmware > v2.0 @@ -356,7 +358,7 @@ def get_pubkey_at_path(self, path): if 'error' in reply: raise DBBError(reply) - if self.is_testnet: + if self.chain != Chain.MAIN: result = {'xpub': xpub_main_2_test(reply['xpub'])} else: result = {'xpub': reply['xpub']} diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 396135b87..ce4b3d2e2 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -17,6 +17,7 @@ common_err_msgs, handle_errors, ) +from ..common import Chain from .btchip.bitcoinTransaction import bitcoinTransaction from .btchip.btchip import btchip from .btchip.btchipComm import ( @@ -162,7 +163,7 @@ def get_pubkey_at_path(self, path): depth = len(path.split("/")) if len(path) > 0 else 0 depth = struct.pack("B", depth) - if self.is_testnet: + if self.chain != Chain.MAIN: version = bytearray.fromhex("043587CF") else: version = bytearray.fromhex("0488B21E") diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 05d3002f6..7b8a8c715 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -53,6 +53,7 @@ is_witness, ser_uint256, ) +from ..common import Chain from .. import bech32 from mnemonic import Mnemonic from usb1 import USBErrorNoDevice @@ -234,7 +235,7 @@ def __init__(self, path, password='', expert=False): self.type = 'Trezor' def _prepare_device(self): - self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' + self.coin_name = 'Bitcoin' if self.chain == Chain.MAIN else 'Testnet' resp = self.client.refresh_features() # If this is a Trezor One or Keepkey, do Initialize if resp.model == '1' or resp.model == 'K1-14AM': @@ -263,7 +264,7 @@ def get_pubkey_at_path(self, path): except ValueError as e: raise BadArgumentError(str(e)) output = btc.get_public_node(self.client, expanded_path, coin_name=self.coin_name) - if self.is_testnet: + if self.chain != Chain.MAIN: result = {'xpub': xpub_main_2_test(output.xpub)} else: result = {'xpub': output.xpub} @@ -341,7 +342,7 @@ def sign_tx(self, tx): p2wsh = True def ignore_input(): - txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0), 0x80000000, 0, 0] + txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (0 if self.chain == Chain.MAIN else 1), 0x80000000, 0, 0] txinputtype.multisig = None txinputtype.script_type = messages.InputScriptType.SPENDWITNESS inputs.append(txinputtype) @@ -399,7 +400,7 @@ def ignore_input(): inputs.append(txinputtype) # address version byte - if self.is_testnet: + if self.chain != Chain.MAIN: p2pkh_version = b'\x6f' p2sh_version = b'\xc4' bech32_hrp = 'tb' @@ -635,7 +636,7 @@ def close(self): # Prompt for a pin on device @trezor_exception def prompt_pin(self): - self.coin_name = 'Testnet' if self.is_testnet else 'Bitcoin' + self.coin_name = 'Bitcoin' if self.chain == Chain.MAIN else 'Testnet' self.client.open() self._prepare_device() if not self.client.features.pin_protection: diff --git a/hwilib/gui.py b/hwilib/gui.py index 40e2c0f37..d48b9d8fa 100644 --- a/hwilib/gui.py +++ b/hwilib/gui.py @@ -10,6 +10,7 @@ from .cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED from .serializations import AddressType +from .common import Chain try: from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog @@ -264,7 +265,7 @@ def attestation_check(self, result: bool) -> None: ) class HWIQt(QMainWindow): - def __init__(self, passphrase='', testnet=False): + def __init__(self, passphrase='', chain=Chain.MAIN): super(HWIQt, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) @@ -274,7 +275,7 @@ def __init__(self, passphrase='', testnet=False): self.client = None self.device_info = {} self.passphrase = passphrase - self.testnet = testnet + self.chain = chain self.current_dialog = None self.getkeypool_opts = { 'start': 0, @@ -354,7 +355,7 @@ def get_client_and_device_info(self, index): # Get the client self.device_info = self.devices[index - 1] self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase) - self.client.is_testnet = self.testnet + self.client.chain = self.chain if self.device_info['type'] == 'bitbox02': self.client.set_noise_config(BitBox02NoiseConfig()) @@ -461,11 +462,9 @@ def toggle_passphrase(self): self.show_sendpindialog(prompt_pin=False) def process_gui_commands(cli_args): - CHAINS = ['main', 'test', 'regtest', 'signet'] - parser = HWIArgumentParser(description='Hardware Wallet Interface Qt, version {}.\nInteractively access and send commands to a hardware wallet device with a GUI. Responses are in JSON format.'.format(__version__)) parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') - parser.add_argument('--chain', help='Select chain to work with ({})'.format(', '.join(CHAINS)), default='main', choices=CHAINS) + parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN) parser.add_argument('--debug', help='Print debug statements', action='store_true') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) @@ -480,8 +479,7 @@ def process_gui_commands(cli_args): # Qt setup app = QApplication() - is_testnet = args.chain in ['test', 'regtest', 'signet'] - window = HWIQt(args.password, is_testnet) + window = HWIQt(args.password, args.chain) window.refresh_clicked() diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 083893d8e..d3c892b28 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -7,6 +7,7 @@ from .base58 import get_xpub_fingerprint_hex from .descriptor import PubkeyProvider from .serializations import AddressType, PSBT +from .common import Chain class HardwareWalletClient(object): @@ -20,7 +21,7 @@ def __init__(self, path: str, password: str, expert: bool) -> None: self.path = path self.password = password self.message_magic = b"\x18Bitcoin Signed Message:\n" - self.is_testnet = False + self.chain = Chain.MAIN self.fingerprint: Optional[str] = None # {bip32_path: } self.xpub_cache: Dict[str, str] = {} From 0265266fc9832d99643c0ee71704de445f36e613 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sat, 13 Feb 2021 18:19:39 -0500 Subject: [PATCH 280/634] ledger: Ensure message signature length is correct --- hwilib/devices/ledger.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index ce4b3d2e2..ed2e3d56d 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -339,15 +339,10 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st # Make signature into standard bitcoin format rLength = signature[3] - r = signature[4: 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - - sig = bytearray(chr(27 + 4 + (signature[0] & 0x01)), 'utf8') + r + s + r = int.from_bytes(signature[4: 4 + rLength], byteorder="big", signed=True) + s = int.from_bytes(signature[4 + rLength + 2:], byteorder="big", signed=True) + + sig = bytearray(chr(27 + 4 + (signature[0] & 0x01)), 'utf8') + r.to_bytes(32, byteorder="big", signed=False) + s.to_bytes(32, byteorder="big", signed=False) return {"signature": base64.b64encode(sig).decode('utf-8')} From 969e5fd740041fb210961d4f18368c5949c4a851 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sat, 13 Feb 2021 18:26:46 -0500 Subject: [PATCH 281/634] test: Verify signed messages and test for short R --- test/test_device.py | 21 ++++++++++++++++++--- test/test_digitalbitbox.py | 3 +-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index caf6e7ad3..f40677b28 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -11,7 +11,7 @@ import unittest from authproxy import AuthServiceProxy, JSONRPCException -from hwilib.base58 import xpub_to_pub_hex +from hwilib.base58 import xpub_to_pub_hex, to_address, decode from hwilib.cli import process_commands from hwilib.descriptor import AddChecksum from hwilib.key import KeyOriginInfo @@ -106,6 +106,7 @@ def do_command(self, args): result = proc.communicate() return json.loads(result[0].decode()) elif self.interface == 'stdin': + args = [f'"{arg}"' for arg in args] input_str = '\n'.join(args) + '\n' proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) result = proc.communicate(input_str.encode()) @@ -603,9 +604,23 @@ def test_display_address_xpub_multisig(self): self.assertEqual(addr[4:58], result['address'][2:56]) class TestSignMessage(DeviceTestCase): + def _check_sign_msg(self, msg): + addr_path = "m/44h/1h/0h/0/0" + sign_res = self.do_command(self.dev_args + ['signmessage', msg, addr_path]) + self.assertNotIn("error", sign_res) + self.assertNotIn("code", sign_res) + self.assertIn("signature", sign_res) + sig = sign_res["signature"] + + addr = self.do_command(self.dev_args + ['displayaddress', '--path', addr_path])["address"] + addr = to_address(decode(addr)[1:-4], b"\x6F") + + self.assertTrue(self.rpc.verifymessage(addr, sig, msg)) + def test_sign_msg(self): - self.do_command(self.dev_args + ['signmessage', '"Message signing test"', 'm/44h/1h/0h/0/0']) + self._check_sign_msg("Message signing test") + self._check_sign_msg("285") # Specific test case for Ledger shorter S def test_bad_path(self): - result = self.do_command(self.dev_args + ['signmessage', '"Message signing test"', 'f']) + result = self.do_command(self.dev_args + ['signmessage', "Message signing test", 'f']) self.assertEquals(result['code'], -7) diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index e64d7da03..56ce32508 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -9,7 +9,7 @@ import time import unittest -from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestGetKeypool, TestGetDescriptors, TestSignTx, TestSignMessage +from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestGetKeypool, TestGetDescriptors, TestSignTx from hwilib.devices.digitalbitbox import BitboxSimulator, send_plain, send_encrypt @@ -157,7 +157,6 @@ def test_getxpub(self): suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) - suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) cleanup_simulator() From 31bf2e91007994973199a53f59f336816d1006f2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 18:02:52 -0500 Subject: [PATCH 282/634] Change display_*_address to return address string Instead of returning a dictionary for HardwareWalletClient.display_single_sig_address and HardwareWalletClient.display_multisig_address, return an address string instead. The conversion to a dictionary for cli output is done by the command handler in commands.py. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 5 ++++- hwilib/commands.py | 9 +++++---- hwilib/devices/bitbox02.py | 8 ++++---- hwilib/devices/coldcard.py | 7 +++---- hwilib/devices/digitalbitbox.py | 3 +-- hwilib/devices/ledger.py | 7 +++---- hwilib/devices/trezor.py | 9 ++++----- hwilib/hwwclient.py | 22 ++++++++++++++-------- 8 files changed, 38 insertions(+), 32 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index e22520e69..06f9a608d 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -29,9 +29,12 @@ NO_DEVICE_TYPE, UNAVAILABLE_ACTION, ) +from .hwwclient import HardwareWalletClient from .serializations import AddressType from . import __version__ +from typing import Dict + import argparse import getpass import logging @@ -41,7 +44,7 @@ def backup_device_handler(args, client): return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) -def displayaddress_handler(args, client): +def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type) def enumerate_handler(args): diff --git a/hwilib/commands.py b/hwilib/commands.py index 18ffc6e76..0d246aedc 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -35,6 +35,7 @@ from .common import Chain from itertools import count +from typing import Dict py_enumerate = enumerate @@ -246,9 +247,9 @@ def getdescriptors(client, account=0): return result -def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH): +def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH) -> Dict[str, str]: if path is not None: - return client.display_singlesig_address(path, addr_type) + return {"address": client.display_singlesig_address(path, addr_type)} elif desc is not None: descriptor = parse_descriptor(desc) addr_type = AddressType.PKH @@ -264,7 +265,7 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres addr_type = AddressType.SH_WPKH elif not is_sh and is_wsh: addr_type = AddressType.WPKH - return client.display_multisig_address(descriptor.thresh, descriptor.pubkeys, addr_type) + return {"address": client.display_multisig_address(descriptor.thresh, descriptor.pubkeys, addr_type)} is_wpkh = isinstance(descriptor, WPKHDescriptor) if isinstance(descriptor, PKHDescriptor) or is_wpkh: pubkey = descriptor.pubkeys[0] @@ -279,7 +280,7 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres addr_type = AddressType.SH_WPKH elif not is_sh and is_wpkh: addr_type = AddressType.WPKH - return client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type) + return {"address": client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type)} def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 3e5c2fbf8..fc528fe3e 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -418,7 +418,7 @@ def display_singlesig_address( self, bip32_path: str, addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: if addr_type == AddressType.SH_WPKH: script_config = bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH @@ -437,7 +437,7 @@ def display_singlesig_address( script_config=script_config, display=True, ) - return {"address": address} + return address @bitbox02_exception def display_multisig_address( @@ -445,7 +445,7 @@ def display_multisig_address( threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: path_suffixes = set(p.deriv_path for p in pubkeys) if len(path_suffixes) != 1: # Path suffix refers to the path after the account-level xpub, usually //
. @@ -480,7 +480,7 @@ def display_multisig_address( address = bb02.btc_address( keypath, coin=self._get_coin(), script_config=script_config, display=True ) - return {"address": address} + return address @bitbox02_exception def sign_tx(self, psbt: PSBT) -> Dict[str, str]: diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 8889a5502..19e702394 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -214,13 +214,12 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st sig = str(base64.b64encode(raw), 'ascii').replace('\n', '') return {"signature": sig} - # Display address of specified type on the device. @coldcard_exception def display_singlesig_address( self, keypath: str, addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') @@ -238,7 +237,7 @@ def display_singlesig_address( if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) - return {'address': address} + return address @coldcard_exception def display_multisig_address( @@ -279,7 +278,7 @@ def display_multisig_address( if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) - return {'address': address} + return address # Setup a new device def setup_device(self, label='', passphrase=''): diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 96bef62ec..f940571ce 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -553,8 +553,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st return {"signature": base64.b64encode(compact_sig).decode('utf-8')} - # Display address of specified type on the device. - def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> Dict[str, str]: + def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> str: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> Dict[str, str]: diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index ce4b3d2e2..7260f77b7 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -351,19 +351,18 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st return {"signature": base64.b64encode(sig).decode('utf-8')} - # Display address of specified type on the device. Only supports single-key based addresses. @ledger_exception def display_singlesig_address( self, keypath: str, addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") p2sh_p2wpkh = addr_type == AddressType.SH_WPKH bech32 = addr_type == AddressType.WPKH output = self.app.getWalletPublicKey(keypath[2:], True, p2sh_p2wpkh or bech32, bech32) - return {'address': output['address'][12:-2]} # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. + return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. @ledger_exception def display_multisig_address( @@ -371,7 +370,7 @@ def display_multisig_address( threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") # Setup a new device diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 7b8a8c715..c8467fc1f 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -511,13 +511,12 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st result = btc.sign_message(self.client, self.coin_name, path, message) return {'signature': base64.b64encode(result.signature).decode('utf-8')} - # Display address of specified type on the device. @trezor_exception def display_singlesig_address( self, keypath: str, addr_type: AddressType, - ) -> Dict[str, str]: + ) -> str: self._check_unlocked() # Script type @@ -539,7 +538,7 @@ def display_singlesig_address( script_type=script_type, multisig=None, ) - return {'address': address} + return address except Exception: pass @@ -551,7 +550,7 @@ def display_multisig_address( threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType - ) -> Dict[str, str]: + ) -> str: self._check_unlocked() pubkey_objs = [] @@ -587,7 +586,7 @@ def display_multisig_address( script_type=script_type, multisig=multisig, ) - return {"address": address} + return address except Exception: pass diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index d3c892b28..9f3aac5c2 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -81,12 +81,14 @@ def display_singlesig_address( self, bip32_path: str, addr_type: AddressType, - ) -> Dict[str, str]: - """Display and return the single sig address of specified type. - - Retrieve the public key at the specified BIP32 derivation path. + ) -> str: + """ + Display and return the single sig address of specified type + at the given derivation path. - Return {"address": }. + :param bip32_path: The BIP 32 derivation path to get the address for + :param addr_type: The address type + :return: The retrieved address also being shown by the device """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") @@ -96,10 +98,14 @@ def display_multisig_address( threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType, - ) -> Dict[str, str]: - """Display and return the multisig address of specified type given the threshold and pubkeys. + ) -> str: + """ + Display and return the multisig address of specified type given the threshold and pubkeys. - Return {"address": }. + :param threshold: The number of signers required in the multisig + :param pubkeys: The public keys, as found in a descriptor, in the multisig + :param addr_type: The address type + :return: The retrieved address also being shown by the device """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 74dc2d35531be0d90735d458bfce756efc7b72d9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 15:59:07 -0500 Subject: [PATCH 283/634] Change get_pubkey_at_path to return ExtendedKey Instead of returning a dictionary for HardwareWalletClient.get_pubkey_at_path, return an ExtendedKey object. The conversion to a dictionary for cli output is done by command handler in commands.py Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 7 +++-- hwilib/commands.py | 17 +++++++---- hwilib/devices/bitbox02.py | 8 ++++-- hwilib/devices/coldcard.py | 17 ++++------- hwilib/devices/digitalbitbox.py | 22 ++++----------- hwilib/devices/ledger.py | 50 +++++++++++---------------------- hwilib/devices/trezor.py | 15 +++------- hwilib/hwwclient.py | 31 ++++++++++++-------- 8 files changed, 70 insertions(+), 97 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 06f9a608d..42daae74a 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -41,6 +41,7 @@ import json import sys + def backup_device_handler(args, client): return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) @@ -50,11 +51,11 @@ def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClien def enumerate_handler(args): return enumerate(password=args.password) -def getmasterxpub_handler(args, client): +def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return getmasterxpub(client) -def getxpub_handler(args, client): - return getxpub(client, path=args.path) +def getxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: + return getxpub(client, path=args.path, expert=args.expert) def getkeypool_handler(args, client): return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, addr_type=args.addr_type, addr_all=args.all) diff --git a/hwilib/commands.py b/hwilib/commands.py index 0d246aedc..4a71efc1e 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -33,6 +33,7 @@ ) from .devices import __all__ as all_devs from .common import Chain +from .hwwclient import HardwareWalletClient from itertools import count from typing import Dict @@ -95,8 +96,8 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False): pass # Ignore things we wouldn't get fingerprints for return None -def getmasterxpub(client): - return client.get_master_xpub() +def getmasterxpub(client: HardwareWalletClient) -> Dict[str, str]: + return {"xpub": client.get_master_xpub().to_string()} def signtx(client, psbt): # Deserialize the transaction @@ -104,8 +105,12 @@ def signtx(client, psbt): tx.deserialize(psbt) return client.sign_tx(tx) -def getxpub(client, path): - return client.get_pubkey_at_path(path) +def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, str]: + xpub = client.get_pubkey_at_path(path) + result = {"xpub": xpub.to_string()} + if expert: + result.update(xpub.get_printable_dict()) + return result def signmessage(client, message, path): return client.sign_message(message, path) @@ -187,7 +192,7 @@ def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=Addre # Get the key at the base if client.xpub_cache.get(path_base) is None: - client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base)['xpub'] + client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base).to_string() pubkey = PubkeyProvider(origin, client.xpub_cache.get(path_base), path_suffix) if is_wpkh: @@ -273,7 +278,7 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} if pubkey.origin.get_fingerprint_hex() != client.get_master_fingerprint_hex(): return {'error': 'Descriptor fingerprint does not match device: ' + desc, 'code': BAD_ARGUMENT} - xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path())['xpub'] + xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path()).to_string() if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} if is_sh and is_wpkh: diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index fc528fe3e..48ada6cf4 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -19,6 +19,7 @@ from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient +from ..key import ExtendedKey from ..serializations import ( AddressType, PSBT, @@ -342,13 +343,14 @@ def _get_xpub(self, keypath: Sequence[int]) -> str: keypath, coin=self._get_coin(), xpub_type=xpub_type, display=False ) - def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: + def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: path_uint32s = parse_path(bip32_path) try: - xpub = self._get_xpub(path_uint32s) + xpub_str = self._get_xpub(path_uint32s) except Bitbox02Exception as exc: raise BitBox02Error(str(exc)) - return {"xpub": xpub} + xpub = ExtendedKey.deserialize(xpub_str) + return xpub def _maybe_register_script_config( self, script_config: bitbox02.btc.BTCScriptConfig, keypath: Sequence[int] diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 19e702394..9a58c49e2 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -39,7 +39,6 @@ ) from ..base58 import ( get_xpub_fingerprint, - xpub_main_2_test, ) from ..key import ( ExtendedKey, @@ -88,22 +87,16 @@ def __init__(self, path, password='', expert=False): device.open_path(path.encode()) self.device = ColdcardDevice(dev=device) - # Must return a dict with the xpub - # Retrieves the public key at the specified BIP 32 derivation path @coldcard_exception - def get_pubkey_at_path(self, path): + def get_pubkey_at_path(self, path: str) -> ExtendedKey: self.device.check_mitm() path = path.replace('h', '\'') path = path.replace('H', '\'') - xpub = self.device.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) + xpub_str = self.device.send_recv(CCProtocolPacker.get_xpub(path), timeout=None) + xpub = ExtendedKey.deserialize(xpub_str) if self.chain != Chain.MAIN: - result = {'xpub': xpub_main_2_test(xpub)} - else: - result = {'xpub': xpub} - if self.expert: - xpub_obj = ExtendedKey.deserialize(xpub) - result.update(xpub_obj.get_printable_dict()) - return result + xpub.version = ExtendedKey.TESTNET_PUBLIC + return xpub def get_master_fingerprint_hex(self): # quick method to get fingerprint of wallet diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index f940571ce..9fe5c843a 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -51,10 +51,6 @@ ser_string, ser_compact_size, ) -from ..base58 import ( - get_xpub_fingerprint, - xpub_main_2_test, -) from ..common import Chain applen = 225280 # flash size minus bootloader length @@ -348,24 +344,18 @@ def __init__(self, path, password, expert=False): self.device.open_path(path.encode()) self.password = password - # Must return a dict with the xpub - # Retrieves the public key at the specified BIP 32 derivation path @digitalbitbox_exception - def get_pubkey_at_path(self, path): + def get_pubkey_at_path(self, path: str) -> ExtendedKey: if '\'' not in path and 'h' not in path and 'H' not in path: raise BadArgumentError('The digital bitbox requires one part of the derivation path to be derived using hardened keys') reply = send_encrypt('{"xpub":"' + path + '"}', self.password, self.device) if 'error' in reply: raise DBBError(reply) + xpub = ExtendedKey.deserialize(reply["xpub"]) if self.chain != Chain.MAIN: - result = {'xpub': xpub_main_2_test(reply['xpub'])} - else: - result = {'xpub': reply['xpub']} - if self.expert: - xpub_obj = ExtendedKey.deserialize(reply['xpub']) - result.update(xpub_obj.get_printable_dict()) - return result + xpub.version = ExtendedKey.TESTNET_PUBLIC + return xpub # Must return a hex string with the signed transaction # The tx must be in the PSBT format @@ -376,7 +366,7 @@ def sign_tx(self, tx): blank_tx = CTransaction(tx.tx) # Get the master key fingerprint - master_fp = get_xpub_fingerprint(self.get_pubkey_at_path('m/0h')['xpub']) + master_fp = self.get_master_fingerprint_hex() # create sighashes sighash_tuples = [] @@ -465,7 +455,7 @@ def sign_tx(self, tx): # Figure out which keypath thing is for this input for pubkey, keypath in psbt_in.hd_keypaths.items(): - if master_fp == keypath.fingerprint: + if master_fp == keypath.fingerprint.hex(): # Add the keypath strings keypath_str = keypath.get_derivation_path() diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 7260f77b7..d1a4c4d00 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -29,14 +29,13 @@ import base64 import hid import struct -from .. import base58 from ..key import ( ExtendedKey, + parse_path, ) from ..serializations import ( AddressType, - hash256, hash160, is_p2sh, is_p2wpkh, @@ -124,10 +123,8 @@ def __init__(self, path, password='', expert=False): self.app = btchip(self.dongle) - # Must return a dict with the xpub - # Retrieves the public key at the specified BIP 32 derivation path @ledger_exception - def get_pubkey_at_path(self, path): + def get_pubkey_at_path(self, path: str) -> ExtendedKey: if not check_keypath(path): raise BadArgumentError("Invalid keypath") path = path[2:] @@ -135,7 +132,8 @@ def get_pubkey_at_path(self, path): path = path.replace('H', '\'') # This call returns raw uncompressed pubkey, chaincode pubkey = self.app.getWalletPublicKey(path) - if path != "": + int_path = parse_path(path) + if len(path) > 0: parent_path = "" for ind in path.split("/")[:-1]: parent_path += ind + "/" @@ -145,38 +143,22 @@ def get_pubkey_at_path(self, path): parent = self.app.getWalletPublicKey(parent_path) fpr = hash160(compress_public_key(parent["publicKey"]))[:4] - # Compute child info - childstr = path.split("/")[-1] - hard = 0 - if childstr[-1] == "'" or childstr[-1] == "h" or childstr[-1] == "H": - childstr = childstr[:-1] - hard = 0x80000000 - child = struct.pack(">I", int(childstr) + hard) + child = int_path[-1] # Special case for m else: - child = bytearray.fromhex("00000000") + child = 0 fpr = child - chainCode = pubkey["chainCode"] - publicKey = compress_public_key(pubkey["publicKey"]) - - depth = len(path.split("/")) if len(path) > 0 else 0 - depth = struct.pack("B", depth) - - if self.chain != Chain.MAIN: - version = bytearray.fromhex("043587CF") - else: - version = bytearray.fromhex("0488B21E") - extkey = version + depth + fpr + child + chainCode + publicKey - checksum = hash256(extkey)[:4] - - xpub = base58.encode(extkey + checksum) - result = {"xpub": xpub} - - if self.expert: - xpub_obj = ExtendedKey.deserialize(xpub) - result.update(xpub_obj.get_printable_dict()) - return result + xpub = ExtendedKey( + version=ExtendedKey.MAINNET_PUBLIC if self.chain == Chain.MAIN else ExtendedKey.TESTNET_PUBLIC, + depth=len(path.split("/")) if len(path) > 0 else 0, + parent_fingerprint=fpr, + child_num=child, + chaincode=pubkey["chainCode"], + privkey=None, + pubkey=compress_public_key(pubkey["publicKey"]), + ) + return xpub # Must return a hex string with the signed transaction # The tx must be in the combined unsigned transaction format diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index c8467fc1f..176e38e79 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -37,7 +37,6 @@ from ..base58 import ( get_xpub_fingerprint, to_address, - xpub_main_2_test, ) from ..key import ( @@ -254,24 +253,18 @@ def _check_unlocked(self): if self.client.features.pin_protection and not self.client.features.unlocked: raise DeviceNotReadyError('{} is locked. Unlock by using \'promptpin\' and then \'sendpin\'.'.format(self.type)) - # Must return a dict with the xpub - # Retrieves the public key at the specified BIP 32 derivation path @trezor_exception - def get_pubkey_at_path(self, path): + def get_pubkey_at_path(self, path: str) -> ExtendedKey: self._check_unlocked() try: expanded_path = parse_path(path) except ValueError as e: raise BadArgumentError(str(e)) output = btc.get_public_node(self.client, expanded_path, coin_name=self.coin_name) + xpub = ExtendedKey.deserialize(output.xpub) if self.chain != Chain.MAIN: - result = {'xpub': xpub_main_2_test(output.xpub)} - else: - result = {'xpub': output.xpub} - if self.expert: - xpub_obj = ExtendedKey.deserialize(output.xpub) - result.update(xpub_obj.get_printable_dict()) - return result + xpub.version = ExtendedKey.TESTNET_PUBLIC + return xpub # Must return a hex string with the signed transaction # The tx must be in the psbt format diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 9f3aac5c2..16b46dfff 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -4,8 +4,8 @@ Optional, Union, ) -from .base58 import get_xpub_fingerprint_hex from .descriptor import PubkeyProvider +from .key import ExtendedKey from .serializations import AddressType, PSBT from .common import Chain @@ -27,28 +27,35 @@ def __init__(self, path: str, password: str, expert: bool) -> None: self.xpub_cache: Dict[str, str] = {} self.expert = expert - def get_master_xpub(self) -> Dict[str, str]: - """Return the master BIP44 public key. + def get_master_xpub(self) -> ExtendedKey: + """ + Get the master BIP 44 public key. - Retrieve the public key at the "m/44h/0h/0h" derivation path. + Retrieves the public key at the "m/44h/0h/0h" derivation path. - Return {"xpub": }. + :return: The extended public key at "m/44h/0h/0h" """ # FIXME testnet is not handled yet return self.get_pubkey_at_path("m/44h/0h/0h") def get_master_fingerprint_hex(self) -> str: - """Return the master public key fingerprint as hex-string. + """ + Get the master public key fingerprint as a hex string. - Retrieve the master public key at the "m/0h" derivation path. + Retrieves the fingerprint of the master public key of a device. + Typically implemented by fetching the extended public key at "m/0h" + and extracting the parent fingerprint from it. + + :return: The fingerprint as a hex string """ - master_xpub = self.get_pubkey_at_path("m/0h")["xpub"] - return get_xpub_fingerprint_hex(master_xpub) + return self.get_pubkey_at_path("m/0h").parent_fingerprint.hex() - def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]: - """Return the public key at the BIP32 derivation path. + def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: + """ + Get the public key at the BIP 32 derivation path. - Return {"xpub": }. + :param bip32_path: The BIP 32 derivation path + :return: The extended public key """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 1aefc2e7e77e977c3de3448c56d6bf2b75d29580 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 17:24:53 -0500 Subject: [PATCH 284/634] Change sign_tx to return PSBT Instead of returning a dictionary for HardwareWalletClient.sign_tx, return a PSBT object instead. The conversion to a dictionary for cli output is done by the command handler in commands.py Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 5 +++-- hwilib/devices/bitbox02.py | 4 ++-- hwilib/devices/coldcard.py | 7 +++---- hwilib/devices/digitalbitbox.py | 9 ++++----- hwilib/devices/ledger.py | 10 ++++------ hwilib/devices/trezor.py | 7 +++---- hwilib/hwwclient.py | 8 +++++--- 8 files changed, 25 insertions(+), 27 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 42daae74a..f5ba894e7 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -76,7 +76,7 @@ def setup_device_handler(args, client): def signmessage_handler(args, client): return signmessage(client, message=args.message, path=args.path) -def signtx_handler(args, client): +def signtx_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return signtx(client, psbt=args.psbt) def wipe_device_handler(args, client): diff --git a/hwilib/commands.py b/hwilib/commands.py index 4a71efc1e..fbe84b067 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -38,6 +38,7 @@ from itertools import count from typing import Dict + py_enumerate = enumerate @@ -99,11 +100,11 @@ def find_device(password='', device_type=None, fingerprint=None, expert=False): def getmasterxpub(client: HardwareWalletClient) -> Dict[str, str]: return {"xpub": client.get_master_xpub().to_string()} -def signtx(client, psbt): +def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, str]: # Deserialize the transaction tx = PSBT() tx.deserialize(psbt) - return client.sign_tx(tx) + return {"psbt": client.sign_tx(tx).serialize()} def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, str]: xpub = client.get_pubkey_at_path(path) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 48ada6cf4..74e25be00 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -485,7 +485,7 @@ def display_multisig_address( return address @bitbox02_exception - def sign_tx(self, psbt: PSBT) -> Dict[str, str]: + def sign_tx(self, psbt: PSBT) -> PSBT: def find_our_key( keypaths: Dict[bytes, KeyOriginInfo] ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: @@ -734,7 +734,7 @@ def script_config_from_utxo( # ser_sig_der() adds SIGHASH_ALL psbt_in.partial_sigs[pubkey] = ser_sig_der(r, s) - return {"psbt": psbt.serialize()} + return psbt def sign_message( self, message: Union[str, bytes], bip32_path: str diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 9a58c49e2..594be8294 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -102,10 +102,8 @@ def get_master_fingerprint_hex(self): # quick method to get fingerprint of wallet return hexlify(struct.pack(' PSBT: self.device.check_mitm() # Get this devices master key fingerprint @@ -173,7 +171,8 @@ def sign_tx(self, tx): tx = PSBT() tx.deserialize(base64.b64encode(result).decode()) - return {'psbt': tx.serialize()} + + return tx @coldcard_exception def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 9fe5c843a..c8d9fc3eb 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -46,6 +46,7 @@ is_p2wpkh, is_p2wsh, is_witness, + PSBT, ser_sig_der, ser_sig_compact, ser_string, @@ -357,10 +358,8 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: xpub.version = ExtendedKey.TESTNET_PUBLIC return xpub - # Must return a hex string with the signed transaction - # The tx must be in the PSBT format @digitalbitbox_exception - def sign_tx(self, tx): + def sign_tx(self, tx: PSBT) -> PSBT: # Create a transaction with all scriptsigs blanekd out blank_tx = CTransaction(tx.tx) @@ -465,7 +464,7 @@ def sign_tx(self, tx): # Return early if nothing to do if len(sighash_tuples) == 0: - return {'psbt': tx.serialize()} + return tx # Sign the sighashes to_send = '{"sign":{"data":[' @@ -504,7 +503,7 @@ def sign_tx(self, tx): for tup, sig in zip(sighash_tuples, der_sigs): tx.inputs[tup[2]].partial_sigs[tup[3]] = sig - return {'psbt': tx.serialize()} + return tx @digitalbitbox_exception def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index d1a4c4d00..13f0bc615 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -36,12 +36,13 @@ ) from ..serializations import ( AddressType, + CTransaction, hash160, is_p2sh, is_p2wpkh, is_p2wsh, is_witness, - CTransaction, + PSBT, ) import logging import re @@ -160,11 +161,8 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: ) return xpub - # Must return a hex string with the signed transaction - # The tx must be in the combined unsigned transaction format - # Current only supports segwit signing @ledger_exception - def sign_tx(self, tx): + def sign_tx(self, tx: PSBT) -> PSBT: c_tx = CTransaction(tx.tx) tx_bytes = c_tx.serialize_with_witness() @@ -303,7 +301,7 @@ def sign_tx(self, tx): first_input = False # Send PSBT back - return {'psbt': tx.serialize()} + return tx @ledger_exception def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 176e38e79..86f252088 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -50,6 +50,7 @@ is_p2sh, is_p2wsh, is_witness, + PSBT, ser_uint256, ) from ..common import Chain @@ -266,10 +267,8 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: xpub.version = ExtendedKey.TESTNET_PUBLIC return xpub - # Must return a hex string with the signed transaction - # The tx must be in the psbt format @trezor_exception - def sign_tx(self, tx): + def sign_tx(self, tx: PSBT) -> PSBT: self._check_unlocked() # Get this devices master key fingerprint @@ -495,7 +494,7 @@ def ignore_input(): p += 1 - return {'psbt': tx.serialize()} + return tx @trezor_exception def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 16b46dfff..e0de3c37c 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -60,10 +60,12 @@ def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def sign_tx(self, psbt: PSBT) -> Dict[str, str]: - """Sign a partially signed bitcoin transaction (PSBT). + def sign_tx(self, psbt: PSBT) -> PSBT: + """ + Sign a partially signed bitcoin transaction (PSBT). - Return {"psbt": }. + :param psbt: The PSBT to sign + :return: The PSBT after being processed by the hardware wallet """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 5d0a42d30e7af1a1971768d4fecc6061551d018d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 17:48:42 -0500 Subject: [PATCH 285/634] Change sign_message to return signature string Instead of returning a dictionary for HardwareWalletClient.sign_message, return a signature string instead. The conversion to a dicitonary for cli output is done by the command handler in commands.py Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 2 +- hwilib/devices/coldcard.py | 4 ++-- hwilib/devices/digitalbitbox.py | 4 ++-- hwilib/devices/ledger.py | 4 ++-- hwilib/devices/trezor.py | 4 ++-- hwilib/hwwclient.py | 16 ++++++++-------- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index f5ba894e7..9ef5a82c1 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -73,7 +73,7 @@ def setup_device_handler(args, client): return setup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) return {'error': 'setup requires interactive mode', 'code': UNAVAILABLE_ACTION} -def signmessage_handler(args, client): +def signmessage_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return signmessage(client, message=args.message, path=args.path) def signtx_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: diff --git a/hwilib/commands.py b/hwilib/commands.py index fbe84b067..ec4efd7f4 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -113,8 +113,8 @@ def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Di result.update(xpub.get_printable_dict()) return result -def signmessage(client, message, path): - return client.sign_message(message, path) +def signmessage(client: HardwareWalletClient, message: str, path: str) -> Dict[str, str]: + return {"signature": client.sign_message(message, path)} def getkeypool_inner(client, path, start, end, internal=False, keypool=True, account=0, addr_type=AddressType.WPKH): diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 74e25be00..d07ab07af 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -738,7 +738,7 @@ def script_config_from_utxo( def sign_message( self, message: Union[str, bytes], bip32_path: str - ) -> Dict[str, str]: + ) -> str: raise UnavailableActionError("The BitBox02 does not support 'signmessage'") @bitbox02_exception diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 594be8294..ab3d757df 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -175,7 +175,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: return tx @coldcard_exception - def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: self.device.check_mitm() keypath = keypath.replace('h', '\'') keypath = keypath.replace('H', '\'') @@ -204,7 +204,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st _, raw = done sig = str(base64.b64encode(raw), 'ascii').replace('\n', '') - return {"signature": sig} + return sig @coldcard_exception def display_singlesig_address( diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index c8d9fc3eb..d1cdd5a76 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -506,7 +506,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: return tx @digitalbitbox_exception - def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: to_hash = b"" to_hash += self.message_magic to_hash += ser_compact_size(len(message)) @@ -540,7 +540,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st compact_sig = ser_sig_compact(r, s, recid) logging.debug(binascii.hexlify(compact_sig)) - return {"signature": base64.b64encode(compact_sig).decode('utf-8')} + return base64.b64encode(compact_sig).decode('utf-8') def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> str: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 13f0bc615..1910c7bea 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -304,7 +304,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: return tx @ledger_exception - def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: if not check_keypath(keypath): raise BadArgumentError("Invalid keypath") if isinstance(message, str): @@ -329,7 +329,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, st sig = bytearray(chr(27 + 4 + (signature[0] & 0x01)), 'utf8') + r + s - return {"signature": base64.b64encode(sig).decode('utf-8')} + return base64.b64encode(sig).decode('utf-8') @ledger_exception def display_singlesig_address( diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 86f252088..2cbf9c40b 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -497,11 +497,11 @@ def ignore_input(): return tx @trezor_exception - def sign_message(self, message: Union[str, bytes], keypath: str) -> Dict[str, str]: + def sign_message(self, message: Union[str, bytes], keypath: str) -> str: self._check_unlocked() path = parse_path(keypath) result = btc.sign_message(self.client, self.coin_name, path, message) - return {'signature': base64.b64encode(result.signature).decode('utf-8')} + return base64.b64encode(result.signature).decode('utf-8') @trezor_exception def display_singlesig_address( diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e0de3c37c..b036aedd2 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -72,16 +72,16 @@ def sign_tx(self, psbt: PSBT) -> PSBT: def sign_message( self, message: Union[str, bytes], bip32_path: str - ) -> Dict[str, str]: - """Sign a message (bitcoin message signing). - - Sign the message according to the bitcoin message signing standard: - usually, the message is a string that is encoded to bytes; - anyway, if the message is already bytes it is processed untouched. + ) -> str: + """ + Sign a message (bitcoin message signing). - Retrieve the signing key at the specified BIP32 derivation path. + Signs a message using the legacy Bitcoin Core signed message format. + The message is signed with the key at the given path. - Return {"signature": }. + :param message: The message to be signed. First encoded as bytes if not already. + :param bip32_path: The BIP 32 derivation for the key to sign the message with. + :return: The signature """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From c794ca9f7bddf2d84012a725f2921b9a2efbb85f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 19:12:02 -0500 Subject: [PATCH 286/634] Change wipe_device to return bool Instead of returning a dictionary for HardwareWalletClient.wipe_device, return a bool representing whether it succeeded. Most failures will jsut raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 4 ++-- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 7 +++---- hwilib/devices/ledger.py | 4 +--- hwilib/devices/trezor.py | 6 ++---- hwilib/hwwclient.py | 12 +++++------- 8 files changed, 17 insertions(+), 25 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 9ef5a82c1..fb66baf66 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -79,7 +79,7 @@ def signmessage_handler(args: argparse.Namespace, client: HardwareWalletClient) def signtx_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return signtx(client, psbt=args.psbt) -def wipe_device_handler(args, client): +def wipe_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return wipe_device(client) def prompt_pin_handler(args, client): diff --git a/hwilib/commands.py b/hwilib/commands.py index ec4efd7f4..b52792983 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -291,8 +291,8 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres def setup_device(client, label='', backup_passphrase=''): return client.setup_device(label, backup_passphrase) -def wipe_device(client): - return client.wipe_device() +def wipe_device(client: HardwareWalletClient) -> Dict[str, bool]: + return {"success": client.wipe_device()} def restore_device(client, label='', word_count=24): return client.restore_device(label, word_count) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index d07ab07af..3e8ef0f13 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -769,8 +769,8 @@ def setup_device( return {"success": bb02.create_backup()} @bitbox02_exception - def wipe_device(self) -> Dict[str, Union[bool, str, int]]: - return {"success": self.init().reset()} + def wipe_device(self) -> bool: + return self.init().reset() @bitbox02_exception def backup_device( diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index ab3d757df..c128ed2a4 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -276,8 +276,7 @@ def display_multisig_address( def setup_device(self, label='', passphrase=''): raise UnavailableActionError('The Coldcard does not support software setup') - # Wipe this device - def wipe_device(self): + def wipe_device(self) -> bool: raise UnavailableActionError('The Coldcard does not support wiping via software') # Restore device from mnemonic or xprv diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index d1cdd5a76..d6fc0b53f 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -573,13 +573,12 @@ def setup_device(self, label='', passphrase=''): return {'success': False, 'error': reply['error']['message']} return {'success': True} - # Wipe this device @digitalbitbox_exception - def wipe_device(self): + def wipe_device(self) -> bool: reply = send_encrypt('{"reset" : "__ERASE__"}', self.password, self.device) if 'error' in reply: - return {'success': False, 'error': reply['error']['message']} - return {'success': True} + raise DeviceFailureError(reply["error"]["message"]) + return True # Restore device from mnemonic or xprv def restore_device(self, label='', word_count=24): diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 1910c7bea..9da2bf912 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,7 +1,6 @@ # Ledger interaction script from typing import ( - Dict, List, Union, ) @@ -357,8 +356,7 @@ def display_multisig_address( def setup_device(self, label='', passphrase=''): raise UnavailableActionError('The Ledger Nano S and X do not support software setup') - # Wipe this device - def wipe_device(self): + def wipe_device(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support wiping via software') # Restore device from mnemonic or xprv diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 2cbf9c40b..d8d1a152e 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,7 +1,6 @@ # Trezor interaction script from typing import ( - Dict, List, Union, ) @@ -597,12 +596,11 @@ def setup_device(self, label='', passphrase=''): device.reset(self.client, passphrase_protection=bool(self.password)) return {'success': True} - # Wipe this device @trezor_exception - def wipe_device(self): + def wipe_device(self) -> bool: self._check_unlocked() device.wipe(self.client) - return {'success': True} + return True # Restore device from mnemonic or xprv @trezor_exception diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index b036aedd2..27bf3c1ed 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -119,14 +119,12 @@ def display_multisig_address( raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def wipe_device(self) -> Dict[str, Union[bool, str, int]]: - """Wipe the HID device. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + def wipe_device(self) -> bool: + """ + Wipe the device. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the wipe was successful + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 96a38df9050822c6dc00c0682e52d7693dd5c842 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 5 Feb 2021 20:47:05 -0500 Subject: [PATCH 287/634] Change setup_device to return bool Instead of returning a dictionary for HardwareWalletClient.setup_device, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 6 +++--- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 7 +++---- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 5 ++--- hwilib/hwwclient.py | 12 +++++------- test/test_keepkey.py | 2 +- test/test_trezor.py | 2 +- 10 files changed, 20 insertions(+), 26 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index fb66baf66..f595e7cce 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -68,7 +68,7 @@ def restore_device_handler(args, client): return restore_device(client, label=args.label, word_count=args.word_count) return {'error': 'restore requires interactive mode', 'code': UNAVAILABLE_ACTION} -def setup_device_handler(args, client): +def setup_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: if args.interactive: return setup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) return {'error': 'setup requires interactive mode', 'code': UNAVAILABLE_ACTION} diff --git a/hwilib/commands.py b/hwilib/commands.py index b52792983..db66eefd3 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -288,8 +288,8 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres addr_type = AddressType.WPKH return {"address": client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type)} -def setup_device(client, label='', backup_passphrase=''): - return client.setup_device(label, backup_passphrase) +def setup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: + return {"success": client.setup_device(label, backup_passphrase)} def wipe_device(client: HardwareWalletClient) -> Dict[str, bool]: return {"success": client.wipe_device()} diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 3e8ef0f13..c94e462c2 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -754,7 +754,7 @@ def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: @bitbox02_exception def setup_device( self, label: str = "", passphrase: str = "" - ) -> Dict[str, Union[bool, str, int]]: + ) -> bool: if passphrase: raise UnavailableActionError( "Passphrase not needed when setting up a BitBox02." @@ -765,8 +765,8 @@ def setup_device( if label: bb02.set_device_name(label) if not bb02.set_password(): - return {"success": False} - return {"success": bb02.create_backup()} + return False + return bb02.create_backup() @bitbox02_exception def wipe_device(self) -> bool: diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index c128ed2a4..e5260016c 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -272,8 +272,7 @@ def display_multisig_address( self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) return address - # Setup a new device - def setup_device(self, label='', passphrase=''): + def setup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The Coldcard does not support software setup') def wipe_device(self) -> bool: diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index d6fc0b53f..805ac3c1d 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -548,9 +548,8 @@ def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> str def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> Dict[str, str]: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') - # Setup a new device @digitalbitbox_exception - def setup_device(self, label='', passphrase=''): + def setup_device(self, label: str = "", passphrase: str = "") -> bool: # Make sure this is not initialized reply = send_encrypt('{"device" : "info"}', self.password, self.device) if 'error' not in reply or ('error' in reply and (reply['error']['code'] != 101 and reply['error']['code'] != '101')): @@ -570,8 +569,8 @@ def setup_device(self, label='', passphrase=''): to_send = {'seed': {'source': 'create', 'key': key, 'filename': backup_filename}} reply = send_encrypt(json.dumps(to_send).encode(), self.password, self.device) if 'error' in reply: - return {'success': False, 'error': reply['error']['message']} - return {'success': True} + raise DeviceFailureError(reply['error']['message']) + return True @digitalbitbox_exception def wipe_device(self) -> bool: diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 9da2bf912..306fdb539 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -352,8 +352,7 @@ def display_multisig_address( ) -> str: raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") - # Setup a new device - def setup_device(self, label='', passphrase=''): + def setup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support software setup') def wipe_device(self) -> bool: diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index d8d1a152e..684d00189 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -583,9 +583,8 @@ def display_multisig_address( raise BadArgumentError("No path supplied matched device keys") - # Setup a new device @trezor_exception - def setup_device(self, label='', passphrase=''): + def setup_device(self, label: str = "", passphrase: str = "") -> bool: self._prepare_device() if not self.simulator: # Use interactive_get_pin @@ -594,7 +593,7 @@ def setup_device(self, label='', passphrase=''): if self.client.features.initialized: raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again') device.reset(self.client, passphrase_protection=bool(self.password)) - return {'success': True} + return True @trezor_exception def wipe_device(self) -> bool: diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 27bf3c1ed..1272c1913 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -131,14 +131,12 @@ def wipe_device(self) -> bool: def setup_device( self, label: str = "", passphrase: str = "" - ) -> Dict[str, Union[bool, str, int]]: - """Setup the HID device. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": str, "code": int}. + ) -> bool: + """ + Setup the device. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the setup was successful + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 79a56efd0..bd54f761f 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -230,7 +230,7 @@ def test_pins(self): self.assertEqual(result['code'], -7) result = self.do_command(self.dev_args + ['sendpin', '00000']) - self.assertFalse(result['success']) + self.assertFalse(result["success"]) # Make sure we get a needs pin message result = self.do_command(self.dev_args + ['getxpub', 'm/0h']) diff --git a/test/test_trezor.py b/test/test_trezor.py index e2d0f2943..950ed657f 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -188,7 +188,7 @@ def test_setup_wipe(self): t_client.client.ui.get_pin = MethodType(get_pin, t_client.client.ui) t_client.client.ui.pin = '1234' result = t_client.setup_device() - self.assertTrue(result['success']) + self.assertTrue(result) # Make sure device is init, setup should fail result = self.do_command(self.dev_args + ['-i', 'setup']) From 8bdee2cd9e9365ea5b24ff379386f35ff3329f3f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 13:50:44 -0500 Subject: [PATCH 288/634] Change restore_device to return bool Instead of returning a dictionary for HardwareWalletClient.restore_device, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 4 ++-- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 3 +-- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 5 ++--- hwilib/hwwclient.py | 12 +++++------- test/test_keepkey.py | 2 +- 9 files changed, 16 insertions(+), 22 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index f595e7cce..59135fd4b 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -63,7 +63,7 @@ def getkeypool_handler(args, client): def getdescriptors_handler(args, client): return getdescriptors(client, account=args.account) -def restore_device_handler(args, client): +def restore_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: if args.interactive: return restore_device(client, label=args.label, word_count=args.word_count) return {'error': 'restore requires interactive mode', 'code': UNAVAILABLE_ACTION} diff --git a/hwilib/commands.py b/hwilib/commands.py index db66eefd3..e4fc94ed3 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -294,8 +294,8 @@ def setup_device(client: HardwareWalletClient, label: str = "", backup_passphras def wipe_device(client: HardwareWalletClient) -> Dict[str, bool]: return {"success": client.wipe_device()} -def restore_device(client, label='', word_count=24): - return client.restore_device(label, word_count) +def restore_device(client: HardwareWalletClient, label: str = "", word_count: int = 24) -> Dict[str, bool]: + return {"success": client.restore_device(label, word_count)} def backup_device(client, label='', backup_passphrase=''): return client.backup_device(label, backup_passphrase) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index c94e462c2..dcb09b996 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -787,11 +787,11 @@ def backup_device( @bitbox02_exception def restore_device( self, label: str = "", word_count: int = 24 - ) -> Dict[str, Union[bool, str, int]]: + ) -> bool: bb02 = self.init(expect_initialized=False) if label: bb02.set_device_name(label) bb02.restore_from_mnemonic() - return {"success": True} + return True diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index e5260016c..5603690bd 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -278,8 +278,7 @@ def setup_device(self, label: str = "", passphrase: str = "") -> bool: def wipe_device(self) -> bool: raise UnavailableActionError('The Coldcard does not support wiping via software') - # Restore device from mnemonic or xprv - def restore_device(self, label='', word_count=24): + def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Coldcard does not support restoring via software') # Begin backup process diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 805ac3c1d..77a9ce0e5 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -579,8 +579,7 @@ def wipe_device(self) -> bool: raise DeviceFailureError(reply["error"]["message"]) return True - # Restore device from mnemonic or xprv - def restore_device(self, label='', word_count=24): + def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Digital Bitbox does not support restoring via software') # Begin backup process diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 306fdb539..1b0fefb29 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -358,8 +358,7 @@ def setup_device(self, label: str = "", passphrase: str = "") -> bool: def wipe_device(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support wiping via software') - # Restore device from mnemonic or xprv - def restore_device(self, label='', word_count=24): + def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support restoring via software') # Begin backup process diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 684d00189..1fe2dfce2 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -601,16 +601,15 @@ def wipe_device(self) -> bool: device.wipe(self.client) return True - # Restore device from mnemonic or xprv @trezor_exception - def restore_device(self, label='', word_count=24): + def restore_device(self, label: str = "", word_count: int = 24) -> bool: self._prepare_device() if not self.simulator: # Use interactive_get_pin self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui) device.recover(self.client, word_count=word_count, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password)) - return {'success': True} + return True # Begin backup process def backup_device(self, label='', passphrase=''): diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 1272c1913..6967181d8 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -143,14 +143,12 @@ def setup_device( def restore_device( self, label: str = "", word_count: int = 24 - ) -> Dict[str, Union[bool, str, int]]: - """Restore the HID device from mnemonic. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + ) -> bool: + """ + Restore the device. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the restore was successful + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") diff --git a/test/test_keepkey.py b/test/test_keepkey.py index bd54f761f..c5403f43b 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -186,7 +186,7 @@ def test_setup_wipe(self): t_client.client.ui.get_pin = MethodType(get_pin, t_client.client.ui) t_client.client.ui.pin = '1234' result = t_client.setup_device() - self.assertTrue(result['success']) + self.assertTrue(result) # Make sure device is init, setup should fail result = self.do_command(self.dev_args + ['-i', 'setup']) From 2d46ebb77eb0822a9c9f06204192a1b48a1290e3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 17:49:23 -0500 Subject: [PATCH 289/634] Change backup_device to return bool Instead of returning a dictionary for HardwareWalletClient.backup_device, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 4 ++-- hwilib/devices/coldcard.py | 5 ++--- hwilib/devices/digitalbitbox.py | 5 ++--- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 3 +-- hwilib/hwwclient.py | 12 +++++------- test/test_coldcard.py | 6 +++--- 9 files changed, 19 insertions(+), 25 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 59135fd4b..0e06d2b19 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -42,7 +42,7 @@ import sys -def backup_device_handler(args, client): +def backup_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: diff --git a/hwilib/commands.py b/hwilib/commands.py index e4fc94ed3..3f2f84cea 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -297,8 +297,8 @@ def wipe_device(client: HardwareWalletClient) -> Dict[str, bool]: def restore_device(client: HardwareWalletClient, label: str = "", word_count: int = 24) -> Dict[str, bool]: return {"success": client.restore_device(label, word_count)} -def backup_device(client, label='', backup_passphrase=''): - return client.backup_device(label, backup_passphrase) +def backup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: + return {"success": client.backup_device(label, backup_passphrase)} def prompt_pin(client): return client.prompt_pin() diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index dcb09b996..52c3a1952 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -775,14 +775,14 @@ def wipe_device(self) -> bool: @bitbox02_exception def backup_device( self, label: str = "", passphrase: str = "" - ) -> Dict[str, Union[bool, str, int]]: + ) -> bool: if label or passphrase: raise UnavailableActionError( "Label/passphrase not needed when exporting mnemonic from the BitBox02." ) self.init().show_mnemonic() - return {"success": True} + return True @bitbox02_exception def restore_device( diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 5603690bd..676d9ca78 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -281,9 +281,8 @@ def wipe_device(self) -> bool: def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Coldcard does not support restoring via software') - # Begin backup process @coldcard_exception - def backup_device(self, label='', passphrase=''): + def backup_device(self, label: str = "", passphrase: str = "") -> bool: self.device.check_mitm() ok = self.device.send_recv(CCProtocolPacker.start_backup()) @@ -309,7 +308,7 @@ def backup_device(self, label='', passphrase=''): result = self.device.download_file(result_len, result_sha, file_number=0) filename = time.strftime('backup-%Y%m%d-%H%M.7z') open(filename, 'wb').write(result) - return {'success': True, 'message': 'The backup has been written to {}'.format(filename)} + return True # Close the device def close(self): diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 77a9ce0e5..80fc1289f 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -582,9 +582,8 @@ def wipe_device(self) -> bool: def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Digital Bitbox does not support restoring via software') - # Begin backup process @digitalbitbox_exception - def backup_device(self, label='', passphrase=''): + def backup_device(self, label: str = "", passphrase: str = "") -> bool: # Need a wallet name and backup passphrase if not label or not passphrase: raise BadArgumentError('The label and backup passphrase for a Digital Bitbox backup must be specified and cannot be empty') @@ -595,7 +594,7 @@ def backup_device(self, label='', passphrase=''): reply = send_encrypt(json.dumps(to_send).encode(), self.password, self.device) if 'error' in reply: raise DBBError(reply) - return {'success': True} + return True # Close the device def close(self): diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 1b0fefb29..9799dabed 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -361,8 +361,7 @@ def wipe_device(self) -> bool: def restore_device(self, label: str = "", word_count: int = 24) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support restoring via software') - # Begin backup process - def backup_device(self, label='', passphrase=''): + def backup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support creating a backup via software') # Close the device diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 1fe2dfce2..ea8d6cd89 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -611,8 +611,7 @@ def restore_device(self, label: str = "", word_count: int = 24) -> bool: device.recover(self.client, word_count=word_count, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password)) return True - # Begin backup process - def backup_device(self, label='', passphrase=''): + def backup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The {} does not support creating a backup via software'.format(self.type)) # Close the device diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 6967181d8..d90a35640 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -155,14 +155,12 @@ def restore_device( def backup_device( self, label: str = "", passphrase: str = "" - ) -> Dict[str, Union[bool, str, int]]: - """Backup the HID device. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + ) -> bool: + """ + Backup the device. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the backup was successful + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") diff --git a/test/test_coldcard.py b/test/test_coldcard.py index e2293835c..f59dc5dfb 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -2,6 +2,7 @@ import argparse import atexit +import glob import os import signal import subprocess @@ -69,9 +70,8 @@ def test_restore(self): def test_backup(self): result = self.do_command(self.dev_args + ['backup']) self.assertTrue(result['success']) - self.assertIn('The backup has been written to', result['message']) - backup_filename = result['message'].split(' ')[-1] - os.remove(backup_filename) + for filename in glob.glob("backup-*.7z"): + os.remove(filename) def test_pin(self): result = self.do_command(self.dev_args + ['promptpin']) From aba9cdeb6200fd86c420609d52ad12e00aa2ab8f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 17:53:06 -0500 Subject: [PATCH 290/634] Change prompt_pin to return bool Instead of returning a dictionary for HardwareWalletClient.prompt_pin, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 2 +- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 3 +-- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 5 ++--- hwilib/hwwclient.py | 12 +++++------- 8 files changed, 14 insertions(+), 20 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 0e06d2b19..9a6ff90b1 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -82,7 +82,7 @@ def signtx_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Di def wipe_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return wipe_device(client) -def prompt_pin_handler(args, client): +def prompt_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return prompt_pin(client) def toggle_passphrase_handler(args, client): diff --git a/hwilib/commands.py b/hwilib/commands.py index 3f2f84cea..70afb8bbb 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -300,8 +300,8 @@ def restore_device(client: HardwareWalletClient, label: str = "", word_count: in def backup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: return {"success": client.backup_device(label, backup_passphrase)} -def prompt_pin(client): - return client.prompt_pin() +def prompt_pin(client: HardwareWalletClient) -> Dict[str, bool]: + return {"success": client.prompt_pin()} def send_pin(client, pin): return client.send_pin(pin) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 52c3a1952..4978ee49c 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -318,7 +318,7 @@ def get_master_fingerprint(self) -> bytes: def get_master_fingerprint_hex(self) -> str: return self.get_master_fingerprint().hex() - def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: + def prompt_pin(self) -> bool: raise UnavailableActionError( "The BitBox02 does not need a PIN sent from the host" ) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 676d9ca78..565101a05 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -314,8 +314,7 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: def close(self): self.device.close() - # Prompt pin - def prompt_pin(self): + def prompt_pin(self) -> bool: raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') # Send pin diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 80fc1289f..35ac2fbdf 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -600,8 +600,7 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: def close(self): self.device.close() - # Prompt pin - def prompt_pin(self): + def prompt_pin(self) -> bool: raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') # Send pin diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 9799dabed..2aeae6870 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -368,8 +368,7 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: def close(self): self.dongle.close() - # Prompt pin - def prompt_pin(self): + def prompt_pin(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') # Send pin diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index ea8d6cd89..2ee8a6a2c 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -619,9 +619,8 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: def close(self): self.client.close() - # Prompt for a pin on device @trezor_exception - def prompt_pin(self): + def prompt_pin(self) -> bool: self.coin_name = 'Bitcoin' if self.chain == Chain.MAIN else 'Testnet' self.client.open() self._prepare_device() @@ -632,7 +631,7 @@ def prompt_pin(self): print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) self.client.call_raw(messages.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=messages.InputScriptType.SPENDADDRESS)) - return {'success': True} + return True # Send the pin @trezor_exception diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index d90a35640..8f845a3f6 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -170,14 +170,12 @@ def close(self) -> None: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def prompt_pin(self) -> Dict[str, Union[bool, str, int]]: - """Prompt for PIN. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + def prompt_pin(self) -> bool: + """ + Prompt for PIN. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the PIN prompt was successful + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 1dbec4adaf5a465f0b19119f63767cf86c77eb8f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 17:56:17 -0500 Subject: [PATCH 291/634] Change send_pin to return bool Instead of returning a dictionary for HardwareWalletClient.send_pin, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 2 +- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 3 +-- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 7 +++---- hwilib/hwwclient.py | 12 +++++------- 8 files changed, 15 insertions(+), 21 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 9a6ff90b1..1dc5a6b58 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -88,7 +88,7 @@ def prompt_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) - def toggle_passphrase_handler(args, client): return toggle_passphrase(client) -def send_pin_handler(args, client): +def send_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return send_pin(client, pin=args.pin) def install_udev_rules_handler(args): diff --git a/hwilib/commands.py b/hwilib/commands.py index 70afb8bbb..b1f804c03 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -303,8 +303,8 @@ def backup_device(client: HardwareWalletClient, label: str = "", backup_passphra def prompt_pin(client: HardwareWalletClient) -> Dict[str, bool]: return {"success": client.prompt_pin()} -def send_pin(client, pin): - return client.send_pin(pin) +def send_pin(client: HardwareWalletClient, pin: str) -> Dict[str, bool]: + return {"success": client.send_pin(pin)} def toggle_passphrase(client): return client.toggle_passphrase() diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 4978ee49c..3f8862e21 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -323,7 +323,7 @@ def prompt_pin(self) -> bool: "The BitBox02 does not need a PIN sent from the host" ) - def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: + def send_pin(self, pin: str) -> bool: raise UnavailableActionError( "The BitBox02 does not need a PIN sent from the host" ) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 565101a05..6c32fb875 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -317,8 +317,7 @@ def close(self): def prompt_pin(self) -> bool: raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') - # Send pin - def send_pin(self, pin): + def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') # Toggle passphrase diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 35ac2fbdf..a0e968ea7 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -603,8 +603,7 @@ def close(self): def prompt_pin(self) -> bool: raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') - # Send pin - def send_pin(self, pin): + def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') # Toggle passphrase diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 2aeae6870..3251faa46 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -371,8 +371,7 @@ def close(self): def prompt_pin(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') - # Send pin - def send_pin(self, pin): + def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') # Toggle passphrase diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 2ee8a6a2c..e72c7d771 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -633,9 +633,8 @@ def prompt_pin(self) -> bool: self.client.call_raw(messages.GetPublicKey(address_n=[0x8000002c, 0x80000001, 0x80000000], ecdsa_curve_name=None, show_display=False, coin_name=self.coin_name, script_type=messages.InputScriptType.SPENDADDRESS)) return True - # Send the pin @trezor_exception - def send_pin(self, pin): + def send_pin(self, pin: str) -> bool: self.client.open() if not pin.isdigit(): raise BadArgumentError("Non-numeric PIN provided") @@ -647,8 +646,8 @@ def send_pin(self, pin): raise DeviceAlreadyUnlockedError('This device does not need a PIN') if self.client.features.unlocked: raise DeviceAlreadyUnlockedError('The PIN has already been sent to this device') - return {'success': False} - return {'success': True} + return False + return True # Toggle passphrase @trezor_exception diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 8f845a3f6..7b103be1b 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -180,14 +180,12 @@ def prompt_pin(self) -> bool: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]: - """Send PIN. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + def send_pin(self, pin: str) -> bool: + """ + Send PIN. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the PIN successfully unlocked the device + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From 2d25b0e018f1f06f16af23bd66cc75950a789e88 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 17:58:45 -0500 Subject: [PATCH 292/634] Change toggle_passphrase to return bool Instead of returning a dictionary for HardwareWalletClient.toggle_passphrase, return a bool representing whether it succeeded. Most failures will just raise an error, but in some cases, False will be returned. Also updates the functions to type check and the docstring in the abstract class. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/devices/bitbox02.py | 4 ++-- hwilib/devices/coldcard.py | 3 +-- hwilib/devices/digitalbitbox.py | 3 +-- hwilib/devices/ledger.py | 3 +-- hwilib/devices/trezor.py | 5 ++--- hwilib/hwwclient.py | 12 +++++------- 8 files changed, 15 insertions(+), 21 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index 1dc5a6b58..f4606d0fa 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -85,7 +85,7 @@ def wipe_device_handler(args: argparse.Namespace, client: HardwareWalletClient) def prompt_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return prompt_pin(client) -def toggle_passphrase_handler(args, client): +def toggle_passphrase_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return toggle_passphrase(client) def send_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: diff --git a/hwilib/commands.py b/hwilib/commands.py index b1f804c03..003d8a217 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -306,8 +306,8 @@ def prompt_pin(client: HardwareWalletClient) -> Dict[str, bool]: def send_pin(client: HardwareWalletClient, pin: str) -> Dict[str, bool]: return {"success": client.send_pin(pin)} -def toggle_passphrase(client): - return client.toggle_passphrase() +def toggle_passphrase(client: HardwareWalletClient) -> Dict[str, bool]: + return {"success": client.toggle_passphrase()} def install_udev_rules(source, location): if platform.system() == "Linux": diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 3f8862e21..84792b80f 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -742,14 +742,14 @@ def sign_message( raise UnavailableActionError("The BitBox02 does not support 'signmessage'") @bitbox02_exception - def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: + def toggle_passphrase(self) -> bool: bb02 = self.init() info = bb02.device_info() if info["mnemonic_passphrase_enabled"]: bb02.disable_mnemonic_passphrase() else: bb02.enable_mnemonic_passphrase() - return {"success": True} + return True @bitbox02_exception def setup_device( diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 6c32fb875..413849e0e 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -320,8 +320,7 @@ def prompt_pin(self) -> bool: def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') - # Toggle passphrase - def toggle_passphrase(self): + def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host') def enumerate(password=''): diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index a0e968ea7..4f41ac373 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -606,8 +606,7 @@ def prompt_pin(self) -> bool: def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') - # Toggle passphrase - def toggle_passphrase(self): + def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host') def enumerate(password=''): diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 3251faa46..b4cd85682 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -374,8 +374,7 @@ def prompt_pin(self) -> bool: def send_pin(self, pin: str) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') - # Toggle passphrase - def toggle_passphrase(self): + def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host') def enumerate(password=''): diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index e72c7d771..f4404e5bc 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -649,9 +649,8 @@ def send_pin(self, pin: str) -> bool: return False return True - # Toggle passphrase @trezor_exception - def toggle_passphrase(self): + def toggle_passphrase(self) -> bool: self._check_unlocked() try: device.apply_settings(self.client, use_passphrase=not self.client.features.passphrase_protection) @@ -660,7 +659,7 @@ def toggle_passphrase(self): print('Confirm the action by entering your PIN', file=sys.stderr) print('Use \'sendpin\' to provide the number positions for the PIN as displayed on your device\'s screen', file=sys.stderr) print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) - return {'success': True} + return True def enumerate(password=''): results = [] diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 7b103be1b..e1f953d9b 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -190,14 +190,12 @@ def send_pin(self, pin: str) -> bool: raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") - def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: - """Toggle passphrase. - - Must return a dictionary with the "success" key, - possibly including also "error" and "code", e.g.: - {"success": bool, "error": srt, "code": int}. + def toggle_passphrase(self) -> bool: + """ + Toggle passphrase. - Raise UnavailableActionError if appropriate for the device. + :return: Whether the passphrase was successfully toggled + :raises UnavailableActionError: if appropriate for the device. """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") From b542472a4508f530586d6c0b998c85782caea063 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 18:32:35 -0500 Subject: [PATCH 293/634] Change UDevInstaller.install to return bool Instead of returning a dictionary for UdevInstaller.install, return a bool representing whether it succeeded. Also updates the functions to type check. --- hwilib/cli.py | 2 +- hwilib/commands.py | 4 ++-- hwilib/errors.py | 4 ++++ hwilib/udevinstaller.py | 9 +++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index f4606d0fa..a7f471e40 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -91,7 +91,7 @@ def toggle_passphrase_handler(args: argparse.Namespace, client: HardwareWalletCl def send_pin_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return send_pin(client, pin=args.pin) -def install_udev_rules_handler(args): +def install_udev_rules_handler(args: argparse.Namespace) -> Dict[str, bool]: return install_udev_rules('udev', args.location) class HWIHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): diff --git a/hwilib/commands.py b/hwilib/commands.py index 003d8a217..f96656b50 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -309,8 +309,8 @@ def send_pin(client: HardwareWalletClient, pin: str) -> Dict[str, bool]: def toggle_passphrase(client: HardwareWalletClient) -> Dict[str, bool]: return {"success": client.toggle_passphrase()} -def install_udev_rules(source, location): +def install_udev_rules(source: str, location: str) -> Dict[str, bool]: if platform.system() == "Linux": from .udevinstaller import UDevInstaller - return UDevInstaller.install(source, location) + return {"success": UDevInstaller.install(source, location)} return {'error': 'udev rules are not needed on your platform', 'code': NOT_IMPLEMENTED} diff --git a/hwilib/errors.py b/hwilib/errors.py index f7ac8f61b..069ae2d63 100644 --- a/hwilib/errors.py +++ b/hwilib/errors.py @@ -91,6 +91,10 @@ class DeviceBusyError(HWWError): def __init__(self, msg: str): HWWError.__init__(self, msg, DEVICE_BUSY) +class NeedsRootError(HWWError): + def __init__(self, msg: str): + HWWError.__init__(self, msg, NEED_TO_BE_ROOT) + @contextmanager def handle_errors( msg: Optional[str] = None, diff --git a/hwilib/udevinstaller.py b/hwilib/udevinstaller.py index cd9f5ae69..c3375204c 100644 --- a/hwilib/udevinstaller.py +++ b/hwilib/udevinstaller.py @@ -1,11 +1,12 @@ +from .errors import NeedsRootError + from subprocess import check_call, CalledProcessError, DEVNULL -from .errors import NEED_TO_BE_ROOT from shutil import copy from os import path, listdir, getlogin, geteuid class UDevInstaller(object): @staticmethod - def install(source, location): + def install(source: str, location: str) -> bool: try: udev_installer = UDevInstaller() udev_installer.copy_udev_rule_files(source, location) @@ -14,9 +15,9 @@ def install(source, location): udev_installer.add_user_plugdev_group() except CalledProcessError: if geteuid() != 0: - return {'error': 'Need to be root.', 'code': NEED_TO_BE_ROOT} + raise NeedsRootError("Need to be root.") raise - return {"success": True} + return True def __init__(self): self._udevadm = '/sbin/udevadm' From db206d52573677f857042aca87d1dd2325493004 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 18:14:05 -0500 Subject: [PATCH 294/634] Add mypy options to mypy.ini --- mypy.ini | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9235ac7f2..b37b51b9f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,15 @@ [mypy] -# Do not type check trezorlib because it does not provide types. -[mypy-hwilib.devices.trezorlib.*] -follow_imports = skip +warn_unused_configs = True +disallow_any_generics = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_return_any = True +implicit_reexport = True +strict_equality = True From 1ef8abddb0ffd68c1c9aabd3c5b3c5153c205d21 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Mon, 8 Feb 2021 18:37:46 -0500 Subject: [PATCH 295/634] Type check hwilib/udevinstaller.py --- hwilib/udevinstaller.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/hwilib/udevinstaller.py b/hwilib/udevinstaller.py index c3375204c..cf7af119b 100644 --- a/hwilib/udevinstaller.py +++ b/hwilib/udevinstaller.py @@ -19,41 +19,41 @@ def install(source: str, location: str) -> bool: raise return True - def __init__(self): + def __init__(self) -> None: self._udevadm = '/sbin/udevadm' self._groupadd = '/usr/sbin/groupadd' self._usermod = '/usr/sbin/usermod' - def _execute(self, command, *args): - command = [command] + list(args) + def _execute(self, cmd: str, *args: str) -> None: + command = [cmd] + list(args) check_call(command, stderr=DEVNULL, stdout=DEVNULL) - def trigger(self): + def trigger(self) -> None: self._execute(self._udevadm, 'trigger') - def reload_rules(self): + def reload_rules(self) -> None: self._execute(self._udevadm, 'control', '--reload-rules') - def add_user_plugdev_group(self): + def add_user_plugdev_group(self) -> None: self._create_group('plugdev') self._add_user_to_group(getlogin(), 'plugdev') - def _create_group(self, name): + def _create_group(self, name: str) -> None: try: self._execute(self._groupadd, name) except CalledProcessError as e: if e.returncode != 9: # group already exists raise - def _add_user_to_group(self, user, group): + def _add_user_to_group(self, user: str, group: str) -> None: self._execute(self._usermod, '-aG', group, user) - def copy_udev_rule_files(self, source, location): + def copy_udev_rule_files(self, source: str, location: str) -> None: src_dir_path = source for rules_file_name in listdir(_resource_path(src_dir_path)): if '.rules' in rules_file_name: rules_file_path = _resource_path(path.join(src_dir_path, rules_file_name)) copy(rules_file_path, location) -def _resource_path(relative_path): +def _resource_path(relative_path: str) -> str: return path.join(path.dirname(__file__), relative_path) From 21b4cbc62e1a13a681b1d61186f24866f2edec71 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 00:05:51 -0500 Subject: [PATCH 296/634] Raise Exceptions instead of returning error dicts The pythonic way to deal with errors is to raise exceptions. Our handle_errors context manager at the cli level deals with turning these into the error dictionaries. There is no need to return error dicts. --- hwilib/commands.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index f96656b50..9e8e72cf9 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -16,10 +16,10 @@ parse_path, ) from .errors import ( + BadArgumentError, + NotImplementedError, UnknownDeviceError, UnavailableActionError, - BAD_ARGUMENT, - NOT_IMPLEMENTED, ) from .descriptor import ( Descriptor, @@ -117,11 +117,7 @@ def signmessage(client: HardwareWalletClient, message: str, path: str) -> Dict[s return {"signature": client.sign_message(message, path)} def getkeypool_inner(client, path, start, end, internal=False, keypool=True, account=0, addr_type=AddressType.WPKH): - - try: - master_fpr = client.get_master_fingerprint_hex() - except NotImplementedError as e: - return {'error': str(e), 'code': NOT_IMPLEMENTED} + master_fpr = client.get_master_fingerprint_hex() desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) @@ -170,9 +166,9 @@ def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=Addre parsed_path.append(0) else: if path[0] != "m": - return {'error': 'Path must start with m/', 'code': BAD_ARGUMENT} + raise BadArgumentError("Path must start with m/") if path[-1] != "*": - return {'error': 'Path must end with /*', 'code': BAD_ARGUMENT} + raise BadArgumentError("Path must end with /*") parsed_path = parse_path(path[:-2]) # Find the last hardened derivation: @@ -215,12 +211,6 @@ def getkeypool(client, path, start, end, internal=False, keypool=True, account=0 for addr_type in addr_types: for internal_addr in [False, True]: chains = chains + getkeypool_inner(client, None, start, end, internal_addr, keypool, account, addr_type) - - # Report the first error we encounter - for chain in chains: - if 'error' in chain: - return chain - # No errors, return pair return chains else: assert len(addr_types) == 1 @@ -228,10 +218,7 @@ def getkeypool(client, path, start, end, internal=False, keypool=True, account=0 def getdescriptors(client, account=0): - try: - master_fpr = client.get_master_fingerprint_hex() - except NotImplementedError as e: - return {'error': str(e), 'code': NOT_IMPLEMENTED} + master_fpr = client.get_master_fingerprint_hex() result = {} @@ -276,12 +263,12 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres if isinstance(descriptor, PKHDescriptor) or is_wpkh: pubkey = descriptor.pubkeys[0] if pubkey.origin is None: - return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT} + raise BadArgumentError(f"Descriptor missing origin info: {desc}") if pubkey.origin.get_fingerprint_hex() != client.get_master_fingerprint_hex(): - return {'error': 'Descriptor fingerprint does not match device: ' + desc, 'code': BAD_ARGUMENT} + raise BadArgumentError(f"Descriptor fingerprint does not match device: {desc}") xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path()).to_string() if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): - return {'error': 'Key in descriptor does not match device: ' + desc, 'code': BAD_ARGUMENT} + raise BadArgumentError(f"Key in descriptor does not match device: {desc}") if is_sh and is_wpkh: addr_type = AddressType.SH_WPKH elif not is_sh and is_wpkh: @@ -313,4 +300,4 @@ def install_udev_rules(source: str, location: str) -> Dict[str, bool]: if platform.system() == "Linux": from .udevinstaller import UDevInstaller return {"success": UDevInstaller.install(source, location)} - return {'error': 'udev rules are not needed on your platform', 'code': NOT_IMPLEMENTED} + raise NotImplementedError("udev rules are not needed on your platform") From 3d1618da3fe585b717e9144f949a23bad21720a4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 00:14:02 -0500 Subject: [PATCH 297/634] Type check hwilib/commands.py --- hwilib/commands.py | 87 +++++++++++++++++++++++++++++++++++++--------- hwilib/key.py | 2 +- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 9e8e72cf9..e2c7f8ee6 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -36,19 +36,24 @@ from .hwwclient import HardwareWalletClient from itertools import count -from typing import Dict +from typing import ( + Any, + Dict, + List, + Optional, +) py_enumerate = enumerate # Get the client for the device -def get_client(device_type, device_path, password='', expert=False): +def get_client(device_type: str, device_path: str, password: str = "", expert: bool = False) -> Optional[HardwareWalletClient]: device_type = device_type.split('_')[0] class_name = device_type.capitalize() module = device_type.lower() - client = None + client: Optional[HardwareWalletClient] = None try: imported_dev = importlib.import_module('.devices.' + module, __package__) client_constructor = getattr(imported_dev, class_name + 'Client') @@ -61,26 +66,35 @@ def get_client(device_type, device_path, password='', expert=False): return client # Get a list of all available hardware wallets -def enumerate(password=''): - result = [] +def enumerate(password: str = "") -> List[Dict[str, Any]]: + result: List[Dict[str, Any]] = [] for module in all_devs: try: imported_dev = importlib.import_module('.devices.' + module, __package__) - result.extend(imported_dev.enumerate(password)) + result.extend(imported_dev.enumerate(password)) # type: ignore except ImportError: pass # Ignore ImportErrors, the user may not have all device dependencies installed return result # Fingerprint or device type required -def find_device(password='', device_type=None, fingerprint=None, expert=False): +def find_device( + password: str = "", + device_type: Optional[str] = None, + fingerprint: Optional[str] = None, + expert: bool = False, +) -> Optional[HardwareWalletClient]: devices = enumerate(password) for d in devices: if device_type is not None and d['type'] != device_type and d['model'] != device_type: continue client = None try: + assert isinstance(d["type"], str) + assert isinstance(d["path"], str) client = get_client(d['type'], d['path'], password, expert) + if client is None: + raise Exception() if fingerprint: master_fpr = d.get('fingerprint', None) @@ -106,9 +120,9 @@ def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, str]: tx.deserialize(psbt) return {"psbt": client.sign_tx(tx).serialize()} -def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, str]: +def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, Any]: xpub = client.get_pubkey_at_path(path) - result = {"xpub": xpub.to_string()} + result: Dict[str, Any] = {"xpub": xpub.to_string()} if expert: result.update(xpub.get_printable_dict()) return result @@ -116,7 +130,16 @@ def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Di def signmessage(client: HardwareWalletClient, message: str, path: str) -> Dict[str, str]: return {"signature": client.sign_message(message, path)} -def getkeypool_inner(client, path, start, end, internal=False, keypool=True, account=0, addr_type=AddressType.WPKH): +def getkeypool_inner( + client: HardwareWalletClient, + path: str, + start: int, + end: int, + internal: bool = False, + keypool: bool = True, + account: int = 0, + addr_type: AddressType = AddressType.WPKH +) -> List[Dict[str, Any]]: master_fpr = client.get_master_fingerprint_hex() desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) @@ -124,7 +147,7 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc if not isinstance(desc, Descriptor): return desc - this_import = {} + this_import: Dict[str, Any] = {} this_import['desc'] = desc.to_string() this_import['range'] = [start, end] @@ -135,7 +158,16 @@ def getkeypool_inner(client, path, start, end, internal=False, keypool=True, acc this_import['watchonly'] = True return [this_import] -def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=AddressType.WPKH, account=0, start=None, end=None): +def getdescriptor( + client: HardwareWalletClient, + master_fpr: str, + path: Optional[str] = None, + internal: bool = False, + addr_type: AddressType = AddressType.WPKH, + account: int = 0, + start: Optional[int] = None, + end: Optional[int] = None +) -> Descriptor: is_wpkh = addr_type is AddressType.WPKH is_sh_wpkh = addr_type is AddressType.SH_WPKH @@ -191,7 +223,7 @@ def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=Addre if client.xpub_cache.get(path_base) is None: client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base).to_string() - pubkey = PubkeyProvider(origin, client.xpub_cache.get(path_base), path_suffix) + pubkey = PubkeyProvider(origin, client.xpub_cache.get(path_base, ""), path_suffix) if is_wpkh: return WPKHDescriptor(pubkey) elif is_sh_wpkh: @@ -199,14 +231,24 @@ def getdescriptor(client, master_fpr, path=None, internal=False, addr_type=Addre else: return PKHDescriptor(pubkey) -def getkeypool(client, path, start, end, internal=False, keypool=True, account=0, addr_type: AddressType = AddressType.PKH, addr_all=False): +def getkeypool( + client: HardwareWalletClient, + path: str, + start: int, + end: int, + internal: bool = False, + keypool: bool = True, + account: int = 0, + addr_type: AddressType = AddressType.PKH, + addr_all: bool = False +) -> List[Dict[str, Any]]: addr_types = [addr_type] if addr_all: addr_types = list(AddressType) # When no specific path or internal-ness is specified, create standard types - chains = [] + chains: List[Dict[str, Any]] = [] if path is None and not internal: for addr_type in addr_types: for internal_addr in [False, True]: @@ -217,7 +259,10 @@ def getkeypool(client, path, start, end, internal=False, keypool=True, account=0 return getkeypool_inner(client, path, start, end, internal, keypool, account, addr_types[0]) -def getdescriptors(client, account=0): +def getdescriptors( + client: HardwareWalletClient, + account: int = 0 +) -> Dict[str, List[str]]: master_fpr = client.get_master_fingerprint_hex() result = {} @@ -240,7 +285,12 @@ def getdescriptors(client, account=0): return result -def displayaddress(client, path=None, desc=None, addr_type: AddressType = AddressType.PKH) -> Dict[str, str]: +def displayaddress( + client: HardwareWalletClient, + path: Optional[str] = None, + desc: Optional[str] = None, + addr_type: AddressType = AddressType.PKH +) -> Dict[str, str]: if path is not None: return {"address": client.display_singlesig_address(path, addr_type)} elif desc is not None: @@ -249,9 +299,11 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres is_sh = isinstance(descriptor, SHDescriptor) is_wsh = isinstance(descriptor, WSHDescriptor) if is_sh or is_wsh: + assert descriptor.subdescriptor descriptor = descriptor.subdescriptor if isinstance(descriptor, WSHDescriptor): is_wsh = True + assert descriptor.subdescriptor descriptor = descriptor.subdescriptor if isinstance(descriptor, MultisigDescriptor): if is_sh and is_wsh: @@ -274,6 +326,7 @@ def displayaddress(client, path=None, desc=None, addr_type: AddressType = Addres elif not is_sh and is_wpkh: addr_type = AddressType.WPKH return {"address": client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type)} + raise BadArgumentError("Missing both path and descriptor") def setup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: return {"success": client.setup_device(label, backup_passphrase)} diff --git a/hwilib/key.py b/hwilib/key.py index a1d12094b..9e2682154 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -264,7 +264,7 @@ def get_full_int_list(self) -> List[int]: return xfp -def parse_path(nstr: str) -> Sequence[int]: +def parse_path(nstr: str) -> List[int]: """ Convert BIP32 path string to list of uint32 integers with hardened flags. Several conventions are supported to set the hardened flag: -1, 1', 1h From cafdd85ba92cbe463e5a000727cae1ec85dd4b02 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 00:33:02 -0500 Subject: [PATCH 298/634] Type check hwilib/cli.py --- hwilib/cli.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/hwilib/cli.py b/hwilib/cli.py index a7f471e40..effc04082 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -27,20 +27,28 @@ HELP_TEXT, MISSING_ARGUMENTS, NO_DEVICE_TYPE, - UNAVAILABLE_ACTION, + UnavailableActionError, + UNKNOWN_ERROR, ) from .hwwclient import HardwareWalletClient from .serializations import AddressType from . import __version__ -from typing import Dict - import argparse import getpass import logging import json import sys +from typing import ( + Any, + Dict, + IO, + List, + NoReturn, + Optional, +) + def backup_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: return backup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) @@ -48,7 +56,7 @@ def backup_device_handler(args: argparse.Namespace, client: HardwareWalletClient def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type) -def enumerate_handler(args): +def enumerate_handler(args: argparse.Namespace) -> List[Dict[str, Any]]: return enumerate(password=args.password) def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: @@ -57,21 +65,21 @@ def getmasterxpub_handler(args: argparse.Namespace, client: HardwareWalletClient def getxpub_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return getxpub(client, path=args.path, expert=args.expert) -def getkeypool_handler(args, client): +def getkeypool_handler(args: argparse.Namespace, client: HardwareWalletClient) -> List[Dict[str, Any]]: return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, addr_type=args.addr_type, addr_all=args.all) -def getdescriptors_handler(args, client): +def getdescriptors_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, List[str]]: return getdescriptors(client, account=args.account) def restore_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: if args.interactive: return restore_device(client, label=args.label, word_count=args.word_count) - return {'error': 'restore requires interactive mode', 'code': UNAVAILABLE_ACTION} + raise UnavailableActionError("restore requires interactive mode") def setup_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]: if args.interactive: return setup_device(client, label=args.label, backup_passphrase=args.backup_passphrase) - return {'error': 'setup requires interactive mode', 'code': UNAVAILABLE_ACTION} + raise UnavailableActionError("setup requires interactive mode") def signmessage_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]: return signmessage(client, message=args.message, path=args.path) @@ -98,36 +106,36 @@ class HWIHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescr pass class HWIArgumentParser(argparse.ArgumentParser): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.formatter_class = HWIHelpFormatter - def print_usage(self, file=None): + def print_usage(self, file: Optional[IO[str]] = None) -> None: if file is None: file = sys.stderr super().print_usage(file) - def print_help(self, file=None): + def print_help(self, file: Optional[IO[str]] = None) -> None: if file is None: file = sys.stderr super().print_help(file) error = {'error': 'Help text requested', 'code': HELP_TEXT} print(json.dumps(error)) - def error(self, message): + def error(self, message: str) -> NoReturn: self.print_usage(sys.stderr) args = {'prog': self.prog, 'message': message} error = {'error': '%(prog)s: error: %(message)s' % args, 'code': MISSING_ARGUMENTS} print(json.dumps(error)) self.exit(2) -def process_commands(cli_args): +def process_commands(cli_args: List[str]) -> Any: parser = HWIArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__)) parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to') parser.add_argument('--device-type', '-t', help='Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.') parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='') parser.add_argument('--stdinpass', help='Enter the device password on the command line', action='store_true') - parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN) + parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN) # type: ignore parser.add_argument('--debug', help='Print debug statements', action='store_true') parser.add_argument('--fingerprint', '-f', help='Specify the device to connect to using the first 4 bytes of the hash160 of the master public key. It will connect to the first device that matches this fingerprint.') parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) @@ -164,7 +172,7 @@ def process_commands(cli_args): kparg_group.add_argument('--nokeypool', action='store_false', dest='keypool', help='Indicates that the keys are not to be imported to the keypool', default=False) getkeypool_parser.add_argument('--internal', action='store_true', help='Indicates that the keys are change keys') kp_type_group = getkeypool_parser.add_mutually_exclusive_group() - kp_type_group.add_argument("--addr-type", help="The address type (and default derivation path) to produce descriptors for", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) + kp_type_group.add_argument("--addr-type", help="The address type (and default derivation path) to produce descriptors for", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) # type: ignore kp_type_group.add_argument('--all', action='store_true', help='Generate addresses for all standard address types (default paths: m/{44,49,84}h/0h/0h/[0,1]/*)') getkeypool_parser.add_argument('--account', help='BIP43 account', type=int, default=0) getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. m/84h/0h/0h/1/* with --addr-type wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') @@ -180,7 +188,7 @@ def process_commands(cli_args): group = displayaddr_parser.add_mutually_exclusive_group(required=True) group.add_argument('--desc', help='Output Descriptor. E.g. wpkh([00000000/84h/0h/0h]xpub.../0/0), where 00000000 must match --fingerprint and xpub can be obtained with getxpub. See doc/descriptors.md in Bitcoin Core') group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. m/84h/0h/0h/1/*') - displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) + displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) # type: ignore displayaddr_parser.set_defaults(func=displayaddress_handler) setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p. Requires interactive mode') @@ -237,7 +245,7 @@ def process_commands(cli_args): device_type = args.device_type password = args.password command = args.command - result = {} + result: Dict[str, Any] = {} # Setup debug logging logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING) @@ -270,6 +278,9 @@ def process_commands(cli_args): else: return {'error': 'You must specify a device type or fingerprint for all commands except enumerate', 'code': NO_DEVICE_TYPE} + if client is None: + return {"error": "Unable to communicated with device", "code": UNKNOWN_ERROR} + client.chain = args.chain # Do the commands @@ -281,6 +292,6 @@ def process_commands(cli_args): return result -def main(): +def main() -> None: result = process_commands(sys.argv[1:]) print(json.dumps(result)) From f0b20b48b56390cdc17f8ac99778363d652c82fe Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 17:25:23 -0500 Subject: [PATCH 299/634] Type check hwilib/bech32.py --- hwilib/bech32.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/hwilib/bech32.py b/hwilib/bech32.py index 68f246874..500f766f2 100644 --- a/hwilib/bech32.py +++ b/hwilib/bech32.py @@ -20,11 +20,18 @@ """Reference implementation for Bech32 and segwit addresses.""" +from typing import ( + List, + Optional, + Tuple, + Union, +) + CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -def bech32_polymod(values): +def bech32_polymod(values: List[int]) -> int: """Internal function that computes the Bech32 checksum.""" generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] chk = 1 @@ -36,30 +43,30 @@ def bech32_polymod(values): return chk -def bech32_hrp_expand(hrp): +def bech32_hrp_expand(hrp: str) -> List[int]: """Expand the HRP into values for checksum computation.""" return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] -def bech32_verify_checksum(hrp, data): +def bech32_verify_checksum(hrp: str, data: List[int]) -> bool: """Verify a checksum given HRP and converted data characters.""" return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 -def bech32_create_checksum(hrp, data): +def bech32_create_checksum(hrp: str, data: List[int]) -> List[int]: """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] -def bech32_encode(hrp, data): +def bech32_encode(hrp: str, data: List[int]) -> str: """Compute a Bech32 string given HRP and data values.""" combined = data + bech32_create_checksum(hrp, data) return hrp + '1' + ''.join([CHARSET[d] for d in combined]) -def bech32_decode(bech): +def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]: """Validate a Bech32 string, and determine HRP and data.""" if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech)): @@ -77,7 +84,7 @@ def bech32_decode(bech): return (hrp, data[:-6]) -def convertbits(data, frombits, tobits, pad=True): +def convertbits(data: Union[bytes, List[int]], frombits: int, tobits: int, pad: bool = True) -> Optional[List[int]]: """General power-of-2 base conversion.""" acc = 0 bits = 0 @@ -100,10 +107,10 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode(hrp, addr): +def decode(hrp: str, addr: str) -> Tuple[Optional[int], Optional[List[int]]]: """Decode a segwit address.""" hrpgot, data = bech32_decode(addr) - if hrpgot != hrp: + if hrpgot != hrp or hrpgot is None or data is None: return (None, None) decoded = convertbits(data[1:], 5, 8, False) if decoded is None or len(decoded) < 2 or len(decoded) > 40: @@ -115,9 +122,12 @@ def decode(hrp, addr): return (data[0], decoded) -def encode(hrp, witver, witprog): +def encode(hrp: str, witver: int, witprog: bytes) -> Optional[str]: """Encode a segwit address.""" - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + conv_bits = convertbits(witprog, 8, 5) + if conv_bits is None: + return None + ret = bech32_encode(hrp, [witver] + conv_bits) if decode(hrp, ret) == (None, None): return None return ret From 83b853592f776611fe7642203e88c59fd684eef6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 17:39:21 -0500 Subject: [PATCH 300/634] Type check hwilib/devices/coldcard.py --- hwilib/devices/coldcard.py | 24 +++++++++++++++--------- mypy.ini | 6 ++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 413849e0e..108337da5 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -56,14 +56,19 @@ import sys import time import struct + from binascii import hexlify, b2a_hex +from typing import ( + Any, + Callable, +) CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock' # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md -def coldcard_exception(f): - def func(*args, **kwargs): +def coldcard_exception(f: Callable[..., Any]) -> Callable[..., Any]: + def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) except CCProtoError as e: @@ -77,7 +82,7 @@ def func(*args, **kwargs): # This class extends the HardwareWalletClient for ColdCard specific things class ColdcardClient(HardwareWalletClient): - def __init__(self, path, password='', expert=False): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: super(ColdcardClient, self).__init__(path, password, expert) # Simulator hard coded pipe socket if path == CC_SIMULATOR_SOCK: @@ -98,7 +103,7 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: xpub.version = ExtendedKey.TESTNET_PUBLIC return xpub - def get_master_fingerprint_hex(self): + def get_master_fingerprint_hex(self) -> str: # quick method to get fingerprint of wallet return hexlify(struct.pack(' Dict[str, str]: + ) -> str: self.device.check_mitm() if addr_type == AddressType.SH_WPKH: @@ -267,6 +273,7 @@ def display_multisig_address( payload = CCProtocolPacker.show_p2sh_address(threshold, xfp_paths, redeem_script, addr_fmt=addr_fmt) address = self.device.send_recv(payload, timeout=None) + assert isinstance(address, str) if self.device.is_simulator: self.device.send_recv(CCProtocolPacker.sim_keypress(b'y')) @@ -310,8 +317,7 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: open(filename, 'wb').write(result) return True - # Close the device - def close(self): + def close(self) -> None: self.device.close() def prompt_pin(self) -> bool: @@ -323,12 +329,12 @@ def send_pin(self, pin: str) -> bool: def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host') -def enumerate(password=''): +def enumerate(password: str = "") -> List[Dict[str, Any]]: results = [] devices = hid.enumerate(COINKITE_VID, CKCC_PID) devices.append({'path': CC_SIMULATOR_SOCK.encode()}) for d in devices: - d_data = {} + d_data: Dict[str, Any] = {} path = d['path'].decode() d_data['type'] = 'coldcard' diff --git a/mypy.ini b/mypy.ini index b37b51b9f..a768a11e1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -13,3 +13,9 @@ warn_unused_ignores = True warn_return_any = True implicit_reexport = True strict_equality = True + +[mypy-hwilib.devices.ckcc.*] +follow_imports = skip + +[mypy-hid] +ignore_missing_imports = True From c8a72e8229dbcc8040f37a3b0f605a0e74aa5153 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 18:38:36 -0500 Subject: [PATCH 301/634] Type check hwilib/devices/digitalbitbox.py --- hwilib/devices/digitalbitbox.py | 117 +++++++++++++++++--------------- mypy.ini | 3 + 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 4f41ac373..3ac4ef561 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -14,8 +14,11 @@ import sys import time from typing import ( + Any, + Callable, Dict, List, + Tuple, Union, ) @@ -67,7 +70,7 @@ DBB_DEVICE_ID = 0x2402 # Errors codes from the device -bad_args = [ +bad_args: List[Union[int, str]] = [ 102, # The password length must be at least " STRINGIFY(PASSWORD_LEN_MIN) " characters. 103, # No input received. 104, # Invalid command. @@ -88,7 +91,7 @@ bad_args.extend([str(x) for x in bad_args]) -device_failures = [ +device_failures: List[Union[int, str]] = [ 101, # Please set a password. 107, # Output buffer overflow. 200, # Seed creation requires an SD card for automatic encrypted backup of the seed. @@ -117,7 +120,7 @@ device_failures.extend([str(x) for x in device_failures]) -cancels = [ +cancels: List[Union[int, str]] = [ 600, # Aborted by user. 601, # Touchbutton timed out. ] @@ -127,21 +130,23 @@ ERR_MEM_SETUP = 503 # Device initialization in progress. class DBBError(Exception): - def __init__(self, error): + def __init__(self, error: Dict[str, Dict[str, Union[str, int]]]) -> None: Exception.__init__(self) self.error = error - def get_error(self): + def get_error(self) -> str: + assert isinstance(self.error["error"]["message"], str) return self.error['error']['message'] - def get_code(self): + def get_code(self) -> Union[str, int]: + assert isinstance(self.error["error"]["code"], int) or isinstance(self.error["error"]["code"], str) return self.error['error']['code'] - def __str__(self): + def __str__(self) -> str: return 'Error: {}, Code: {}'.format(self.error['error']['message'], self.error['error']['code']) -def digitalbitbox_exception(f): - def func(*args, **kwargs): +def digitalbitbox_exception(f: Callable[..., Any]) -> Any: + def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) except DBBError as e: @@ -156,47 +161,49 @@ def func(*args, **kwargs): return func -def aes_encrypt_with_iv(key, iv, data): +def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes = pyaes.Encrypter(aes_cbc) e = aes.feed(data) + aes.feed() # empty aes.feed() appends pkcs padding + assert isinstance(e, bytes) return e -def aes_decrypt_with_iv(key, iv, data): +def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes = pyaes.Decrypter(aes_cbc) s = aes.feed(data) + aes.feed() # empty aes.feed() strips pkcs padding + assert isinstance(s, bytes) return s -def encrypt_aes(secret, s): +def encrypt_aes(secret: bytes, s: bytes) -> bytes: iv = bytes(os.urandom(16)) ct = aes_encrypt_with_iv(secret, iv, s) e = iv + ct return e -def decrypt_aes(secret, e): +def decrypt_aes(secret: bytes, e: bytes) -> bytes: iv, e = e[:16], e[16:] s = aes_decrypt_with_iv(secret, iv, e) return s -def sha256(x): +def sha256(x: bytes) -> bytes: return hashlib.sha256(x).digest() -def sha512(x): +def sha512(x: bytes) -> bytes: return hashlib.sha512(x).digest() -def double_hash(x): - if type(x) is not bytearray: +def double_hash(x: Union[str, bytes]) -> bytes: + if not isinstance(x, bytes): x = x.encode('utf-8') return sha256(sha256(x)) -def derive_keys(x): +def derive_keys(x: str) -> Tuple[bytes, bytes]: h = double_hash(x) h = sha512(h) return (h[:len(h) // 2], h[len(h) // 2:]) -def to_string(x, enc): +def to_string(x: Union[str, bytes, bytearray], enc: str) -> str: if isinstance(x, (bytes, bytearray)): return x.decode(enc) if isinstance(x, str): @@ -205,30 +212,32 @@ def to_string(x, enc): raise DeviceFailureError("Not a string or bytes like object") class BitboxSimulator(): - def __init__(self, ip, port): + def __init__(self, ip: str, port: int) -> None: self.ip = ip self.port = port self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.connect((self.ip, self.port)) self.socket.settimeout(1) - def send_recv(self, msg): + def send_recv(self, msg: bytes) -> bytes: self.socket.sendall(msg) data = self.socket.recv(3584) return data - def close(self): + def close(self) -> None: self.socket.close() - def get_serial_number_string(self): + def get_serial_number_string(self) -> str: return 'dbb_fw:v5.0.0' -def send_frame(data, device): +Device = Union[BitboxSimulator, hid.device] + +def send_frame(data: bytes, device: hid.device) -> None: data = bytearray(data) data_len = len(data) seq = 0 idx = 0 - write = [] + write = b"" while idx < data_len: if idx == 0: # INIT frame @@ -242,7 +251,7 @@ def send_frame(data, device): idx += len(write) -def read_frame(device): +def read_frame(device: hid.device) -> bytes: # INIT response read = bytearray(device.read(usb_report_size)) cid = ((read[0] * 256 + read[1]) * 256 + read[2]) * 256 + read[3] @@ -259,15 +268,14 @@ def read_frame(device): assert cmd == HWW_CMD, '- USB command frame mismatch' return data -def get_firmware_version(device): +def get_firmware_version(device: Device) -> Tuple[int, int, int]: serial_number = device.get_serial_number_string() split_serial = serial_number.split(':') firm_ver = split_serial[1][1:] # Version is vX.Y.Z, we just need X.Y.Z split_ver = firm_ver.split('.') return (int(split_ver[0]), int(split_ver[1]), int(split_ver[2])) # major, minor, revision -def send_plain(msg, device): - reply = "" +def send_plain(msg: bytes, device: Device) -> Dict[str, Any]: try: if isinstance(device, BitboxSimulator): r = device.send_recv(msg) @@ -275,7 +283,7 @@ def send_plain(msg, device): firm_ver = get_firmware_version(device) if (firm_ver[0] == 2 and firm_ver[1] == 0) or (firm_ver[0] == 1): hidBufSize = 4096 - device.write('\0' + msg + '\0' * (hidBufSize - len(msg))) + device.write(b"\0" + msg + b"\0" * (hidBufSize - len(msg))) r = bytearray() while len(r) < hidBufSize: r += bytearray(device.read(hidBufSize)) @@ -284,14 +292,14 @@ def send_plain(msg, device): r = read_frame(device) r = r.rstrip(b' \t\r\n\0') r = r.replace(b"\0", b'') - r = to_string(r, 'utf8') - reply = json.loads(r) + result = json.loads(to_string(r, "utf8")) + assert isinstance(result, dict) + return result except Exception as e: - reply = json.loads('{"error":"Exception caught while sending plaintext message to DigitalBitbox ' + str(e) + '"}') - return reply + return {"error": f"Exception caught while sending plaintext message to DigitalBitbox {str(e)}"} -def send_encrypt(msg, password, device): - reply = "" +def send_encrypt(message: str, password: str, device: Device) -> Dict[str, Any]: + msg = message.encode("utf8") try: firm_ver = get_firmware_version(device) if firm_ver[0] >= 5: @@ -313,25 +321,26 @@ def send_encrypt(msg, password, device): raise Exception("Failed to validate HMAC") else: msg = b64_unencoded - reply = decrypt_aes(encryption_key, msg) - reply = json.loads(reply.decode("utf-8")) - if 'error' in reply: - password = None + plaintext = decrypt_aes(encryption_key, msg) + result = json.loads(plaintext.decode("utf-8")) + assert isinstance(result, dict) + return result + else: + return reply except Exception as e: - reply = {'error': 'Exception caught while sending encrypted message to DigitalBitbox ' + str(e)} - return reply + return {'error': 'Exception caught while sending encrypted message to DigitalBitbox ' + str(e)} -def stretch_backup_key(password): +def stretch_backup_key(password: str) -> str: key = hashlib.pbkdf2_hmac('sha512', password.encode(), b'Digital Bitbox', 20480) return binascii.hexlify(key).decode() -def format_backup_filename(name): +def format_backup_filename(name: str) -> str: return '{}-{}.pdf'.format(name, time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())) # This class extends the HardwareWalletClient for Digital Bitbox specific things class DigitalbitboxClient(HardwareWalletClient): - def __init__(self, path, password, expert=False): + def __init__(self, path: str, password: str, expert: bool = False) -> None: super(DigitalbitboxClient, self).__init__(path, password, expert) if not password: raise NoPasswordError('Password must be supplied for digital BitBox') @@ -339,7 +348,7 @@ def __init__(self, path, password, expert=False): split_path = path.split(':') ip = split_path[1] port = int(split_path[2]) - self.device = BitboxSimulator(ip, port) + self.device: Device = BitboxSimulator(ip, port) else: self.device = hid.device() self.device.open_path(path.encode()) @@ -416,6 +425,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: sighash += hash256(ser_tx) txin.scriptSig = b"" else: + assert psbt_in.witness_utxo is not None # Calculate hashPrevouts and hashSequence prevouts_preimage = b"" sequence_preimage = b"" @@ -545,7 +555,7 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> str: def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> str: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') - def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> Dict[str, str]: + def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> str: raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') @digitalbitbox_exception @@ -560,14 +570,14 @@ def setup_device(self, label: str = "", passphrase: str = "") -> bool: raise BadArgumentError('The label and backup passphrase for a new Digital Bitbox wallet must be specified and cannot be empty') # Set password - to_send = {'password': self.password} + to_send: Dict[str, Any] = {'password': self.password} reply = send_plain(json.dumps(to_send).encode(), self.device) # Now make the wallet key = stretch_backup_key(passphrase) backup_filename = format_backup_filename(label) to_send = {'seed': {'source': 'create', 'key': key, 'filename': backup_filename}} - reply = send_encrypt(json.dumps(to_send).encode(), self.password, self.device) + reply = send_encrypt(json.dumps(to_send), self.password, self.device) if 'error' in reply: raise DeviceFailureError(reply['error']['message']) return True @@ -591,13 +601,12 @@ def backup_device(self, label: str = "", passphrase: str = "") -> bool: key = stretch_backup_key(passphrase) backup_filename = format_backup_filename(label) to_send = {'backup': {'source': 'all', 'key': key, 'filename': backup_filename}} - reply = send_encrypt(json.dumps(to_send).encode(), self.password, self.device) + reply = send_encrypt(json.dumps(to_send), self.password, self.device) if 'error' in reply: raise DBBError(reply) return True - # Close the device - def close(self): + def close(self) -> None: self.device.close() def prompt_pin(self) -> bool: @@ -609,7 +618,7 @@ def send_pin(self, pin: str) -> bool: def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host') -def enumerate(password=''): +def enumerate(password: str = "") -> List[Dict[str, Any]]: results = [] devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID) # Try connecting to simulator @@ -623,7 +632,7 @@ def enumerate(password=''): for d in devices: if ('interface_number' in d and d['interface_number'] == 0 or ('usage_page' in d and d['usage_page'] == 0xffff)): - d_data = {} + d_data: Dict[str, Any] = {} path = d['path'].decode() d_data['type'] = 'digitalbitbox' diff --git a/mypy.ini b/mypy.ini index a768a11e1..e583ef892 100644 --- a/mypy.ini +++ b/mypy.ini @@ -19,3 +19,6 @@ follow_imports = skip [mypy-hid] ignore_missing_imports = True + +[mypy-pyaes] +ignore_missing_imports = True From d63d45642608ab1913e929e25b8046e007c47529 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 9 Feb 2021 19:36:01 -0500 Subject: [PATCH 302/634] Type check hwilib/devices/trezor.py --- hwilib/devices/trezor.py | 59 ++++++++++++++++++++++++---------------- mypy.ini | 9 ++++++ 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index f4404e5bc..378a24924 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,7 +1,14 @@ # Trezor interaction script from typing import ( + Any, + Callable, + Dict, List, + NoReturn, + Optional, + Sequence, + Tuple, Union, ) from ..descriptor import PubkeyProvider @@ -72,8 +79,11 @@ 1 2 3 """.strip() +Device = Union[hid.HidTransport, webusb.WebUsbTransport, udp.UdpTransport] + + # Only handles up to 15 of 15 -def parse_multisig(script): +def parse_multisig(script: bytes) -> Tuple[bool, Optional[messages.MultisigRedeemScriptType]]: # Get m m = script[0] - 80 if m < 1 or m > 15: @@ -107,8 +117,8 @@ def parse_multisig(script): return (True, multisig) -def trezor_exception(f): - def func(*args, **kwargs): +def trezor_exception(f: Callable[..., Any]) -> Any: + def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) except ValueError as e: @@ -120,7 +130,7 @@ def func(*args, **kwargs): return func -def interactive_get_pin(self, code=None): +def interactive_get_pin(self: object, code: Optional[int] = None) -> str: if code == messages.PinMatrixRequestType.Currrent: desc = "current PIN" elif code == messages.PinMatrixRequestType.NewFirst: @@ -140,13 +150,12 @@ def interactive_get_pin(self, code=None): return pin -def mnemonic_words(expand=False, language="english"): +def mnemonic_words(expand: bool = False, language: str = "english") -> Callable[[Any], str]: + wordlist: Sequence[str] = [] if expand: wordlist = Mnemonic(language).wordlist - else: - wordlist = set() - def expand_word(word): + def expand_word(word: str) -> str: if not expand: return word if word in wordlist: @@ -157,7 +166,7 @@ def expand_word(word): print("Choose one of: " + ", ".join(matches), file=sys.stderr) raise KeyError(word) - def get_word(type): + def get_word(type: messages.WordRequestType) -> str: assert type == messages.WordRequestType.Plain while True: try: @@ -172,26 +181,26 @@ def get_word(type): class PassphraseUI: - def __init__(self, passphrase): + def __init__(self, passphrase: str) -> None: self.passphrase = passphrase self.pinmatrix_shown = False self.prompt_shown = False self.always_prompt = False self.return_passphrase = True - def button_request(self, code): + def button_request(self, code: Optional[int]) -> None: if not self.prompt_shown: print("Please confirm action on your Trezor device", file=sys.stderr) if not self.always_prompt: self.prompt_shown = True - def get_pin(self, code=None): + def get_pin(self, code: Optional[int] = None) -> NoReturn: raise NotImplementedError('get_pin is not needed') - def disallow_passphrase(self): + def disallow_passphrase(self) -> None: self.return_passphrase = False - def get_passphrase(self): + def get_passphrase(self) -> str: if self.return_passphrase: return self.passphrase raise ValueError('Passphrase from Host is not allowed for Trezor T') @@ -201,7 +210,7 @@ def get_passphrase(self): WEBUSB_IDS = TREZORS.copy() -def get_path_transport(path: str): +def get_path_transport(path: str) -> Device: devs = hid.HidTransport.enumerate(usb_ids=HID_IDS) devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=WEBUSB_IDS)) devs.extend(udp.UdpTransport.enumerate()) @@ -214,7 +223,7 @@ def get_path_transport(path: str): # This class extends the HardwareWalletClient for Trezor specific things class TrezorClient(HardwareWalletClient): - def __init__(self, path, password='', expert=False): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: super(TrezorClient, self).__init__(path, password, expert) self.simulator = False transport = get_path_transport(path) @@ -233,7 +242,7 @@ def __init__(self, path, password='', expert=False): self.password = password self.type = 'Trezor' - def _prepare_device(self): + def _prepare_device(self) -> None: self.coin_name = 'Bitcoin' if self.chain == Chain.MAIN else 'Testnet' resp = self.client.refresh_features() # If this is a Trezor One or Keepkey, do Initialize @@ -246,7 +255,7 @@ def _prepare_device(self): except TrezorFailure: self.client.init_device() - def _check_unlocked(self): + def _check_unlocked(self) -> None: self._prepare_device() if self.client.features.model == 'T' and isinstance(self.client.ui, PassphraseUI): self.client.ui.disallow_passphrase() @@ -332,7 +341,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: scriptcode = psbt_in.witness_script p2wsh = True - def ignore_input(): + def ignore_input() -> None: txinputtype.address_n = [0x80000000 | 84, 0x80000000 | (0 if self.chain == Chain.MAIN else 1), 0x80000000, 0, 0] txinputtype.multisig = None txinputtype.script_type = messages.InputScriptType.SPENDWITNESS @@ -468,6 +477,7 @@ def ignore_input(): ) t.bin_outputs.append(o) logging.debug(psbt_in.non_witness_utxo.hash) + assert psbt_in.non_witness_utxo.sha256 is not None prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t # Sign the transaction @@ -529,6 +539,7 @@ def display_singlesig_address( script_type=script_type, multisig=None, ) + assert isinstance(address, str) return address except Exception: pass @@ -549,7 +560,7 @@ def display_multisig_address( if p.extkey is not None: xpub = p.extkey hd_node = messages.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey) - pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path))) + pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=parse_path("m" + p.deriv_path if p.deriv_path is not None else ""))) else: hd_node = messages.HDNodeType(depth=0, fingerprint=0, child_num=0, chain_code=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', public_key=p.get_pubkey_bytes(0)) pubkey_objs.append(messages.HDNodePathType(node=hd_node, address_n=[])) @@ -577,6 +588,7 @@ def display_multisig_address( script_type=script_type, multisig=multisig, ) + assert isinstance(address, str) return address except Exception: pass @@ -614,9 +626,8 @@ def restore_device(self, label: str = "", word_count: int = 24) -> bool: def backup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The {} does not support creating a backup via software'.format(self.type)) - # Close the device @trezor_exception - def close(self): + def close(self) -> None: self.client.close() @trezor_exception @@ -661,13 +672,13 @@ def toggle_passphrase(self) -> bool: print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) return True -def enumerate(password=''): +def enumerate(password: str = "") -> List[Dict[str, Any]]: results = [] devs = hid.HidTransport.enumerate() devs.extend(webusb.WebUsbTransport.enumerate()) devs.extend(udp.UdpTransport.enumerate()) for dev in devs: - d_data = {} + d_data: Dict[str, Any] = {} d_data['type'] = 'trezor' d_data['path'] = dev.get_path() diff --git a/mypy.ini b/mypy.ini index e583ef892..476425a75 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,8 +17,17 @@ strict_equality = True [mypy-hwilib.devices.ckcc.*] follow_imports = skip +[mypy-hwilib.devices.trezorlib.*] +follow_imports = skip + [mypy-hid] ignore_missing_imports = True [mypy-pyaes] ignore_missing_imports = True + +[mypy-usb1] +ignore_missing_imports = True + +[mypy-mnemonic] +ignore_missing_imports = True From 237b2f5897a5c60b198028c0beb96deaad38b006 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 11 Feb 2021 16:49:33 -0500 Subject: [PATCH 303/634] Type check hwilib/devices/keepkey.py --- hwilib/devices/keepkey.py | 45 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 4db1390ce..29359b535 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -20,7 +20,12 @@ ResetDevice, ) -from typing import Dict +from typing import ( + Any, + Dict, + List, + Optional, +) py_enumerate = enumerate # Need to use the enumerate built-in but there's another function already named that @@ -31,20 +36,20 @@ WEBUSB_IDS.update(KEEPKEY_WEBUSB_IDS) -class KeepkeyFeatures(Features): +class KeepkeyFeatures(Features): # type: ignore def __init__( self, *, - firmware_variant: str = None, - firmware_hash: bytes = None, - **kwargs, + firmware_variant: Optional[str] = None, + firmware_hash: Optional[bytes] = None, + **kwargs: Any, ) -> None: super().__init__(**kwargs) self.firmware_variant = firmware_variant self.firmware_hash = firmware_hash @classmethod - def get_fields(cls) -> Dict: + def get_fields(cls) -> Dict[int, p.FieldInfo]: return { 1: ('vendor', p.UnicodeType, None), 2: ('major_version', p.UVarintType, None), @@ -69,18 +74,18 @@ def get_fields(cls) -> Dict: } -class KeepkeyResetDevice(ResetDevice): +class KeepkeyResetDevice(ResetDevice): # type: ignore def __init__( self, *, - auto_lock_delay_ms: int = None, - **kwargs, + auto_lock_delay_ms: Optional[int] = None, + **kwargs: Any, ) -> None: super().__init__(**kwargs) self.auto_lock_delay_ms = auto_lock_delay_ms @classmethod - def get_fields(cls) -> Dict: + def get_fields(cls) -> Dict[int, p.FieldInfo]: return { 1: ('display_random', p.BoolType, None), 2: ('strength', p.UVarintType, 256), # default=256 @@ -94,15 +99,15 @@ def get_fields(cls) -> Dict: } -class KeepkeyDebugLinkState(DebugLinkState): +class KeepkeyDebugLinkState(DebugLinkState): # type: ignore def __init__( self, *, - recovery_cipher: str = None, - recovery_auto_completed_word: str = None, - firmware_hash: bytes = None, - storage_hash: bytes = None, - **kwargs, + recovery_cipher: Optional[str] = None, + recovery_auto_completed_word: Optional[str] = None, + firmware_hash: Optional[bytes] = None, + storage_hash: Optional[bytes] = None, + **kwargs: Any, ) -> None: super().__init__(**kwargs) self.recovery_cipher = recovery_cipher @@ -111,7 +116,7 @@ def __init__( self.storage_hash = storage_hash @classmethod - def get_fields(cls) -> Dict: + def get_fields(cls) -> Dict[int, p.FieldType]: return { 1: ('layout', p.BytesType, None), 2: ('pin', p.UnicodeType, None), @@ -131,7 +136,7 @@ def get_fields(cls) -> Dict: class KeepkeyClient(TrezorClient): - def __init__(self, path, password='', expert=False): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: super(KeepkeyClient, self).__init__(path, password, expert) self.type = 'Keepkey' self.client.vendors = ("keepkey.com") @@ -142,13 +147,13 @@ def __init__(self, path, password='', expert=False): self.client.debug.map_type_to_class_override[KeepkeyDebugLinkState.MESSAGE_WIRE_TYPE] = KeepkeyDebugLinkState -def enumerate(password=''): +def enumerate(password: str = "") -> List[Dict[str, Any]]: results = [] devs = hid.HidTransport.enumerate(usb_ids=KEEPKEY_HID_IDS) devs.extend(webusb.WebUsbTransport.enumerate(usb_ids=KEEPKEY_WEBUSB_IDS)) devs.extend(udp.UdpTransport.enumerate()) for dev in devs: - d_data = {} + d_data: Dict[str, Any] = {} d_data['type'] = 'keepkey' d_data['model'] = 'keepkey' From d276f8e1c8428a4f65ac50edbd17ba387c5cc5ca Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 11 Feb 2021 18:27:42 -0500 Subject: [PATCH 304/634] Type check hwilib/devices/ledger.py --- hwilib/devices/ledger.py | 35 ++++++++++++++++++----------------- mypy.ini | 3 +++ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index b4cd85682..18d24a718 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,8 +1,12 @@ # Ledger interaction script from typing import ( + Any, + Callable, + Dict, List, Union, + Tuple, ) from ..descriptor import PubkeyProvider @@ -59,7 +63,7 @@ } # minimal checking of string keypath -def check_keypath(key_path): +def check_keypath(key_path: str) -> bool: parts = re.split("/", key_path) if parts[0] != "m": return False @@ -84,8 +88,8 @@ def check_keypath(key_path): 0x6985, # BTCHIP_SW_CONDITIONS_OF_USE_NOT_SATISFIED ] -def ledger_exception(f): - def func(*args, **kwargs): +def ledger_exception(f: Callable[..., Any]) -> Any: + def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) except ValueError as e: @@ -106,7 +110,7 @@ def func(*args, **kwargs): # This class extends the HardwareWalletClient for Ledger Nano S and Nano X specific things class LedgerClient(HardwareWalletClient): - def __init__(self, path, password='', expert=False): + def __init__(self, path: str, password: str = "", expert: bool = False) -> None: super(LedgerClient, self).__init__(path, password, expert) if path.startswith('tcp'): @@ -147,7 +151,7 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: # Special case for m else: child = 0 - fpr = child + fpr = b"\x00\x00\x00\x00" xpub = ExtendedKey( version=ExtendedKey.MAINNET_PUBLIC if self.chain == Chain.MAIN else ExtendedKey.TESTNET_PUBLIC, @@ -168,7 +172,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: # Master key fingerprint master_fpr = hash160(compress_public_key(self.app.getWalletPublicKey('')["publicKey"]))[:4] # An entry per input, each with 0 to many keys to sign with - all_signature_attempts = [[]] * len(c_tx.vin) + all_signature_attempts: List[List[Tuple[str, bytes]]] = [[]] * len(c_tx.vin) # Get the app version to determine whether to use Trusted Input for segwit version = self.app.getFirmwareVersion() @@ -184,7 +188,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: has_segwit = False has_legacy = False - script_codes = [[]] * len(c_tx.vin) + script_codes: List[bytes] = [b""] * len(c_tx.vin) # Detect changepath, (p2sh-)p2(w)pkh only change_path = '' @@ -203,11 +207,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: for txin, psbt_in, i_num in zip(c_tx.vin, tx.inputs, range(len(c_tx.vin))): - seq = format(txin.nSequence, 'x') - seq = seq.zfill(8) - seq = bytearray.fromhex(seq) - seq.reverse() - seq_hex = ''.join('{:02x}'.format(x) for x in seq) + seq_hex = txin.nSequence.to_bytes(4, byteorder="little").hex() scriptcode = b"" utxo = None @@ -243,6 +243,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: else: # We only need legacy inputs in the case where all inputs are legacy, we check # later + assert psbt_in.non_witness_utxo is not None ledger_prevtx = bitcoinTransaction(psbt_in.non_witness_utxo.serialize()) legacy_inputs.append(self.app.getTrustedInput(ledger_prevtx, txin.prevout.n)) legacy_inputs[-1]["sequence"] = seq_hex @@ -269,7 +270,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: if master_fpr == keypath.fingerprint: # Add the keypath strings keypath_str = keypath.get_derivation_path()[2:] # Drop the leading m/ - signature_attempts.append([keypath_str, pubkey]) + signature_attempts.append((keypath_str, pubkey)) all_signature_attempts[i_num] = signature_attempts @@ -341,6 +342,7 @@ def display_singlesig_address( p2sh_p2wpkh = addr_type == AddressType.SH_WPKH bech32 = addr_type == AddressType.WPKH output = self.app.getWalletPublicKey(keypath[2:], True, p2sh_p2wpkh or bech32, bech32) + assert isinstance(output["address"], str) return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. @ledger_exception @@ -364,8 +366,7 @@ def restore_device(self, label: str = "", word_count: int = 24) -> bool: def backup_device(self, label: str = "", passphrase: str = "") -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support creating a backup via software') - # Close the device - def close(self): + def close(self) -> None: self.dongle.close() def prompt_pin(self) -> bool: @@ -377,7 +378,7 @@ def send_pin(self, pin: str) -> bool: def toggle_passphrase(self) -> bool: raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host') -def enumerate(password=''): +def enumerate(password: str = '') -> List[Dict[str, Any]]: results = [] devices = [] devices.extend(hid.enumerate(LEDGER_VENDOR_ID, 0)) @@ -386,7 +387,7 @@ def enumerate(password=''): for d in devices: if ('interface_number' in d and d['interface_number'] == 0 or ('usage_page' in d and d['usage_page'] == 0xffa0)): - d_data = {} + d_data: Dict[str, Any] = {} path = d['path'].decode() d_data['type'] = 'ledger' diff --git a/mypy.ini b/mypy.ini index 476425a75..e4e8d06b5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -20,6 +20,9 @@ follow_imports = skip [mypy-hwilib.devices.trezorlib.*] follow_imports = skip +[mypy-hwilib.devices.btchip.*] +follow_imports = skip + [mypy-hid] ignore_missing_imports = True From ff33638858ecd55198136b3b81407be7b3aeac92 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 11 Feb 2021 18:37:27 -0500 Subject: [PATCH 305/634] Update cirrus to type check the other files --- .cirrus.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 9be23ba04..46d8a77c3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -128,14 +128,26 @@ basic_test_task: - name: Type Check type_check_script: > poetry run - mypy --implicit-reexport --strict + mypy + hwi.py hwilib/base58.py + hwilib/bech32.py + hwilib/cli.py + hwilib/commands.py + hwilib/descriptor.py + hwilib/devices/bitbox02.py + hwilib/devices/coldcard.py + hwilib/devices/digitalbitbox.py + hwilib/devices/__init__.py + hwilib/devices/keepkey.py + hwilib/devices/ledger.py + hwilib/devices/trezor.py hwilib/errors.py - hwilib/serializations.py hwilib/hwwclient.py - hwilib/devices/bitbox02.py + hwilib/__init__.py hwilib/key.py - hwilib/descriptor.py + hwilib/serializations.py + hwilib/udevinstaller.py - name: Non-Device Tests test_script: cd test; poetry run ./run_tests.py; cd .. From ad1146dfa91d83e16bc3ef461281ea23d27a4262 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 11 Feb 2021 19:00:50 -0500 Subject: [PATCH 306/634] Remove unnecssary type: ignore from hwilib/devices/bitbox02.py --- hwilib/devices/bitbox02.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 84792b80f..f04e9df2c 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -47,7 +47,7 @@ ) from ..common import Chain -import hid # type: ignore +import hid from bitbox02 import util from bitbox02 import bitbox02 From 5c9b218629de1a7f253a78efe910bc2e603bd6b0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 15:45:06 -0500 Subject: [PATCH 307/634] Make cli and gui modules internal Moves cli and gui to _cli and _gui. These modules should not be publicly accessible so they are made to be private. --- .cirrus.yml | 2 +- hwi-qt.py | 2 +- hwi.py | 2 +- hwilib/{cli.py => _cli.py} | 0 hwilib/{gui.py => _gui.py} | 2 +- pyproject.toml | 4 ++-- test/test_coldcard.py | 2 +- test/test_device.py | 2 +- test/test_keepkey.py | 2 +- test/test_ledger.py | 2 +- test/test_trezor.py | 2 +- test/test_udevrules.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) rename hwilib/{cli.py => _cli.py} (100%) rename hwilib/{gui.py => _gui.py} (99%) diff --git a/.cirrus.yml b/.cirrus.yml index 46d8a77c3..58a836e1c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -132,7 +132,7 @@ basic_test_task: hwi.py hwilib/base58.py hwilib/bech32.py - hwilib/cli.py + hwilib/_cli.py hwilib/commands.py hwilib/descriptor.py hwilib/devices/bitbox02.py diff --git a/hwi-qt.py b/hwi-qt.py index edf24b359..1da390381 100755 --- a/hwi-qt.py +++ b/hwi-qt.py @@ -1,7 +1,7 @@ #! /usr/bin/env python3 if __name__ == '__main__': - from hwilib.gui import main + from hwilib._gui import main main() else: raise ImportError('hwi-qt is not importable. Import hwilib instead') diff --git a/hwi.py b/hwi.py index 23a1e3032..7a59887cf 100755 --- a/hwi.py +++ b/hwi.py @@ -3,7 +3,7 @@ # Hardware wallet interaction script if __name__ == '__main__': - from hwilib.cli import main + from hwilib._cli import main main() else: raise ImportError('hwi is not importable. Import hwilib instead') diff --git a/hwilib/cli.py b/hwilib/_cli.py similarity index 100% rename from hwilib/cli.py rename to hwilib/_cli.py diff --git a/hwilib/gui.py b/hwilib/_gui.py similarity index 99% rename from hwilib/gui.py rename to hwilib/_gui.py index d48b9d8fa..186a2eee0 100644 --- a/hwilib/gui.py +++ b/hwilib/_gui.py @@ -7,7 +7,7 @@ from typing import Callable from . import commands, __version__ -from .cli import HWIArgumentParser +from ._cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED from .serializations import AddressType from .common import Chain diff --git a/pyproject.toml b/pyproject.toml index ef806e288..1ddebddf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ flake8 = "^3.7" mypy = "^0.790" [tool.poetry.scripts] -hwi = 'hwilib.cli:main' -hwi-qt = 'hwilib.gui:main' +hwi = 'hwilib._cli:main' +hwi-qt = 'hwilib._gui:main' [build-system] requires = ["poetry>=0.12"] diff --git a/test/test_coldcard.py b/test/test_coldcard.py index f59dc5dfb..7b7b7ec47 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -10,7 +10,7 @@ import time import unittest -from hwilib.cli import process_commands +from hwilib._cli import process_commands from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx def coldcard_test_suite(simulator, rpc, userpass, interface): diff --git a/test/test_device.py b/test/test_device.py index f40677b28..87665ab7b 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -12,7 +12,7 @@ from authproxy import AuthServiceProxy, JSONRPCException from hwilib.base58 import xpub_to_pub_hex, to_address, decode -from hwilib.cli import process_commands +from hwilib._cli import process_commands from hwilib.descriptor import AddChecksum from hwilib.key import KeyOriginInfo from hwilib.serializations import PSBT diff --git a/test/test_keepkey.py b/test/test_keepkey.py index c5403f43b..97118dca4 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -16,7 +16,7 @@ from hwilib.devices.trezorlib import device, messages from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx -from hwilib.cli import process_commands +from hwilib._cli import process_commands from hwilib.devices.keepkey import ( KeepkeyClient, KeepkeyDebugLinkState, diff --git a/test/test_ledger.py b/test/test_ledger.py index cdddfe0a0..bf4cc28ea 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -11,7 +11,7 @@ from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx -from hwilib.cli import process_commands +from hwilib._cli import process_commands class LedgerEmulator(DeviceEmulator): def __init__(self, path): diff --git a/test/test_trezor.py b/test/test_trezor.py index 950ed657f..56dfadb54 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -17,7 +17,7 @@ from hwilib.devices.trezorlib import device, messages from test_device import DeviceEmulator, DeviceTestCase, start_bitcoind, TestDeviceConnect, TestDisplayAddress, TestGetKeypool, TestGetDescriptors, TestSignMessage, TestSignTx -from hwilib.cli import process_commands +from hwilib._cli import process_commands from hwilib.devices.trezor import TrezorClient from types import MethodType diff --git a/test/test_udevrules.py b/test/test_udevrules.py index 4cb292a3a..110b2422e 100755 --- a/test/test_udevrules.py +++ b/test/test_udevrules.py @@ -3,7 +3,7 @@ import unittest import filecmp from os import makedirs, remove, removedirs, walk, path -from hwilib.cli import process_commands +from hwilib._cli import process_commands class TestUdevRulesInstaller(unittest.TestCase): INSTALLATION_FOLDER = 'rules.d' From b153d704b53045d3c60b21e4a21828893fae1124 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 15:55:26 -0500 Subject: [PATCH 308/634] Make base58 private Moves base58 to _base58 --- .cirrus.yml | 2 +- hwilib/{base58.py => _base58.py} | 0 hwilib/commands.py | 2 +- hwilib/devices/coldcard.py | 2 +- hwilib/devices/trezor.py | 2 +- hwilib/key.py | 2 +- test/test_base58.py | 2 +- test/test_device.py | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename hwilib/{base58.py => _base58.py} (100%) diff --git a/.cirrus.yml b/.cirrus.yml index 58a836e1c..55c4e64da 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -130,7 +130,7 @@ basic_test_task: poetry run mypy hwi.py - hwilib/base58.py + hwilib/_base58.py hwilib/bech32.py hwilib/_cli.py hwilib/commands.py diff --git a/hwilib/base58.py b/hwilib/_base58.py similarity index 100% rename from hwilib/base58.py rename to hwilib/_base58.py diff --git a/hwilib/commands.py b/hwilib/commands.py index e2c7f8ee6..9326878b2 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -7,7 +7,7 @@ import platform from .serializations import AddressType, PSBT -from .base58 import xpub_to_pub_hex +from ._base58 import xpub_to_pub_hex from .key import ( H_, HARDENED_FLAG, diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 108337da5..721d6998d 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -37,7 +37,7 @@ AF_P2SH, AF_P2WSH_P2SH, ) -from ..base58 import ( +from .._base58 import ( get_xpub_fingerprint, ) from ..key import ( diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 378a24924..f3bc89572 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -40,7 +40,7 @@ device, ) from .trezorlib import messages -from ..base58 import ( +from .._base58 import ( get_xpub_fingerprint, to_address, ) diff --git a/hwilib/key.py b/hwilib/key.py index 9e2682154..73d5d3a2b 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -from . import base58 +from . import _base58 as base58 from .errors import BadArgumentError import binascii diff --git a/test/test_base58.py b/test/test_base58.py index a988dce50..aff01dc45 100755 --- a/test/test_base58.py +++ b/test/test_base58.py @@ -5,7 +5,7 @@ from binascii import unhexlify from typing import List, Tuple import unittest -import hwilib.base58 as base58 +import hwilib._base58 as base58 # Taken from Bitcoin Core # https://github.com/bitcoin/bitcoin/blob/master/src/test/data/base58_encode_decode.json diff --git a/test/test_device.py b/test/test_device.py index 87665ab7b..b4be090e1 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -11,7 +11,7 @@ import unittest from authproxy import AuthServiceProxy, JSONRPCException -from hwilib.base58 import xpub_to_pub_hex, to_address, decode +from hwilib._base58 import xpub_to_pub_hex, to_address, decode from hwilib._cli import process_commands from hwilib.descriptor import AddChecksum from hwilib.key import KeyOriginInfo From 68996ab959799f569a653d26fdadf040578d0133 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 15:58:30 -0500 Subject: [PATCH 309/634] Make bech32 private Moves bech32 to _bech32 --- .cirrus.yml | 2 +- hwilib/{bech32.py => _bech32.py} | 0 hwilib/devices/trezor.py | 2 +- test/test_bech32.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename hwilib/{bech32.py => _bech32.py} (100%) diff --git a/.cirrus.yml b/.cirrus.yml index 55c4e64da..7a15c99a6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -131,7 +131,7 @@ basic_test_task: mypy hwi.py hwilib/_base58.py - hwilib/bech32.py + hwilib/_bech32.py hwilib/_cli.py hwilib/commands.py hwilib/descriptor.py diff --git a/hwilib/bech32.py b/hwilib/_bech32.py similarity index 100% rename from hwilib/bech32.py rename to hwilib/_bech32.py diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index f3bc89572..adeeabaa3 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -60,7 +60,7 @@ ser_uint256, ) from ..common import Chain -from .. import bech32 +from .. import _bech32 as bech32 from mnemonic import Mnemonic from usb1 import USBErrorNoDevice from types import MethodType diff --git a/test/test_bech32.py b/test/test_bech32.py index 6f0dff0d7..d834e8ae9 100755 --- a/test/test_bech32.py +++ b/test/test_bech32.py @@ -25,7 +25,7 @@ import binascii import unittest -import hwilib.bech32 as segwit_addr +import hwilib._bech32 as segwit_addr def segwit_scriptpubkey(witver, witprog): """Construct a Segwit scriptPubKey for a given witness program.""" From b052173296e53421fe0e1b56de919856d2a1389a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 19 Feb 2021 16:18:26 -0500 Subject: [PATCH 310/634] Replace get_master_fingerprint_hex to get_master_fingerprint Handle master fingerprint as bytes instead of hex string. --- hwilib/commands.py | 8 ++++---- hwilib/devices/bitbox02.py | 5 +---- hwilib/devices/coldcard.py | 8 ++++---- hwilib/devices/digitalbitbox.py | 4 ++-- hwilib/devices/keepkey.py | 2 +- hwilib/devices/ledger.py | 2 +- hwilib/devices/trezor.py | 2 +- hwilib/hwwclient.py | 8 ++++---- 8 files changed, 18 insertions(+), 21 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index e2c7f8ee6..01d0ba54b 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -99,7 +99,7 @@ def find_device( if fingerprint: master_fpr = d.get('fingerprint', None) if master_fpr is None: - master_fpr = client.get_master_fingerprint_hex() + master_fpr = client.get_master_fingerprint().hex() if master_fpr != fingerprint: client.close() @@ -140,7 +140,7 @@ def getkeypool_inner( account: int = 0, addr_type: AddressType = AddressType.WPKH ) -> List[Dict[str, Any]]: - master_fpr = client.get_master_fingerprint_hex() + master_fpr = client.get_master_fingerprint().hex() desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) @@ -263,7 +263,7 @@ def getdescriptors( client: HardwareWalletClient, account: int = 0 ) -> Dict[str, List[str]]: - master_fpr = client.get_master_fingerprint_hex() + master_fpr = client.get_master_fingerprint().hex() result = {} @@ -316,7 +316,7 @@ def displayaddress( pubkey = descriptor.pubkeys[0] if pubkey.origin is None: raise BadArgumentError(f"Descriptor missing origin info: {desc}") - if pubkey.origin.get_fingerprint_hex() != client.get_master_fingerprint_hex(): + if pubkey.origin.fingerprint != client.get_master_fingerprint(): raise BadArgumentError(f"Descriptor fingerprint does not match device: {desc}") xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path()).to_string() if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub): diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index f04e9df2c..cba39b3e3 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -211,7 +211,7 @@ def enumerate(password: str = "") -> List[Dict[str, object]]: if _using_external_gui else "Please use any subcommand to unlock" ) - d_data["fingerprint"] = client.get_master_fingerprint_hex() + d_data["fingerprint"] = client.get_master_fingerprint().hex() result.append(d_data) @@ -315,9 +315,6 @@ def get_master_fingerprint(self) -> bytes: bb02 = self.init() return bb02.root_fingerprint() - def get_master_fingerprint_hex(self) -> str: - return self.get_master_fingerprint().hex() - def prompt_pin(self) -> bool: raise UnavailableActionError( "The BitBox02 does not need a PIN sent from the host" diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 108337da5..76beaa351 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -57,7 +57,7 @@ import time import struct -from binascii import hexlify, b2a_hex +from binascii import b2a_hex from typing import ( Any, Callable, @@ -103,9 +103,9 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: xpub.version = ExtendedKey.TESTNET_PUBLIC return xpub - def get_master_fingerprint_hex(self) -> str: + def get_master_fingerprint(self) -> bytes: # quick method to get fingerprint of wallet - return hexlify(struct.pack(' PSBT: @@ -350,7 +350,7 @@ def enumerate(password: str = "") -> List[Dict[str, Any]]: with handle_errors(common_err_msgs["enumerate"], d_data): try: client = ColdcardClient(path) - d_data['fingerprint'] = client.get_master_fingerprint_hex() + d_data['fingerprint'] = client.get_master_fingerprint().hex() except RuntimeError as e: # Skip the simulator if it's not there if str(e) == 'Cannot connect to simulator. Is it running?': diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 3ac4ef561..831d72bb8 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -374,7 +374,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: blank_tx = CTransaction(tx.tx) # Get the master key fingerprint - master_fp = self.get_master_fingerprint_hex() + master_fp = self.get_master_fingerprint().hex() # create sighashes sighash_tuples = [] @@ -651,7 +651,7 @@ def enumerate(password: str = "") -> List[Dict[str, Any]]: d_data['error'] = 'Not initialized' d_data['code'] = DEVICE_NOT_INITIALIZED else: - d_data['fingerprint'] = client.get_master_fingerprint_hex() + d_data['fingerprint'] = client.get_master_fingerprint().hex() d_data['needs_pin_sent'] = False d_data['needs_passphrase_sent'] = True diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index 29359b535..ceb2645c7 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -180,7 +180,7 @@ def enumerate(password: str = "") -> List[Dict[str, Any]]: if d_data['needs_passphrase_sent'] and not password: raise DeviceNotReadyError("Passphrase needs to be specified before the fingerprint information can be retrieved") if client.client.features.initialized: - d_data['fingerprint'] = client.get_master_fingerprint_hex() + d_data['fingerprint'] = client.get_master_fingerprint().hex() d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent else: d_data['error'] = 'Not initialized' diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index eafa1cd94..43b994927 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -402,7 +402,7 @@ def enumerate(password: str = '') -> List[Dict[str, Any]]: with handle_errors(common_err_msgs["enumerate"], d_data): try: client = LedgerClient(path, password) - d_data['fingerprint'] = client.get_master_fingerprint_hex() + d_data['fingerprint'] = client.get_master_fingerprint().hex() d_data['needs_pin_sent'] = False d_data['needs_passphrase_sent'] = False except BTChipException: diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 378a24924..9e21efb2e 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -707,7 +707,7 @@ def enumerate(password: str = "") -> List[Dict[str, Any]]: if d_data['needs_passphrase_sent'] and not password: raise DeviceNotReadyError("Passphrase needs to be specified before the fingerprint information can be retrieved") if client.client.features.initialized: - d_data['fingerprint'] = client.get_master_fingerprint_hex() + d_data['fingerprint'] = client.get_master_fingerprint().hex() d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent else: d_data['error'] = 'Not initialized' diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e1f953d9b..7d7d0cf50 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -38,17 +38,17 @@ def get_master_xpub(self) -> ExtendedKey: # FIXME testnet is not handled yet return self.get_pubkey_at_path("m/44h/0h/0h") - def get_master_fingerprint_hex(self) -> str: + def get_master_fingerprint(self) -> bytes: """ - Get the master public key fingerprint as a hex string. + Get the master public key fingerprint as bytes. Retrieves the fingerprint of the master public key of a device. Typically implemented by fetching the extended public key at "m/0h" and extracting the parent fingerprint from it. - :return: The fingerprint as a hex string + :return: The fingerprint as bytes """ - return self.get_pubkey_at_path("m/0h").parent_fingerprint.hex() + return self.get_pubkey_at_path("m/0h").parent_fingerprint def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: """ From c7dd5155b25a3106b24622864bbd95f6fdda1587 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 19 Feb 2021 16:19:48 -0500 Subject: [PATCH 311/634] Change descriptors get_fingerprint_hex() to fingerprint.hex() --- hwilib/key.py | 6 ------ test/test_descriptor.py | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/hwilib/key.py b/hwilib/key.py index 9e2682154..5b461f821 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -248,12 +248,6 @@ def get_derivation_path(self) -> str: """ return "m" + self._path_string() - def get_fingerprint_hex(self) -> str: - """ - Return the hex for just the fingerprint - """ - return binascii.hexlify(self.fingerprint).decode() - def get_full_int_list(self) -> List[int]: """ Return a list of ints representing this KeyOriginInfo. diff --git a/test/test_descriptor.py b/test/test_descriptor.py index e65495cc7..c1a71feb2 100755 --- a/test/test_descriptor.py +++ b/test/test_descriptor.py @@ -18,7 +18,7 @@ def test_parse_descriptor_with_origin(self): d = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WPKHDescriptor)) - self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") @@ -33,12 +33,12 @@ def test_parse_multisig_descriptor_with_origin(self): desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WSHDescriptor)) self.assertTrue(isinstance(desc.subdescriptor, MultisigDescriptor)) - self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.subdescriptor.pubkeys[0].deriv_path, "/0/0") - self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.fingerprint.hex(), "00000002") self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") self.assertEqual(desc.subdescriptor.pubkeys[1].deriv_path, "/0/0") @@ -52,12 +52,12 @@ def test_parse_multisig_descriptor_with_origin(self): desc = parse_descriptor(d) self.assertTrue(isinstance(desc, SHDescriptor)) self.assertTrue(isinstance(desc.subdescriptor, MultisigDescriptor)) - self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.subdescriptor.pubkeys[0].deriv_path, "/0/0") - self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.pubkeys[1].origin.fingerprint.hex(), "00000002") self.assertEqual(desc.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") self.assertEqual(desc.subdescriptor.pubkeys[1].deriv_path, "/0/0") @@ -72,12 +72,12 @@ def test_parse_multisig_descriptor_with_origin(self): self.assertTrue(isinstance(desc, SHDescriptor)) self.assertTrue(isinstance(desc.subdescriptor, WSHDescriptor)) self.assertTrue(isinstance(desc.subdescriptor.subdescriptor, MultisigDescriptor)) - self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[0].deriv_path, "/0/0") - self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].origin.get_fingerprint_hex(), "00000002") + self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].origin.fingerprint.hex(), "00000002") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].origin.get_derivation_path(), "m/48'/0'/0'/2'") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") self.assertEqual(desc.subdescriptor.subdescriptor.pubkeys[1].deriv_path, "/0/0") @@ -104,7 +104,7 @@ def test_parse_descriptor_with_origin_fingerprint_only(self): d = "wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WPKHDescriptor)) - self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(len(desc.pubkeys[0].origin.path), 0) self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") @@ -118,7 +118,7 @@ def test_parse_descriptor_with_key_at_end_with_origin(self): d = "wpkh([00000001/84'/1'/0'/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, WPKHDescriptor)) - self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'/0/0") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) @@ -131,7 +131,7 @@ def test_parse_descriptor_with_key_at_end_with_origin(self): d = "pkh([00000001/84'/1'/0'/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, PKHDescriptor)) - self.assertEqual(desc.pubkeys[0].origin.get_fingerprint_hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84'/1'/0'/0/0") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) From 0d4b807c9c2629eff63b42d8a70431235e926e99 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 19 Feb 2021 16:58:03 -0500 Subject: [PATCH 312/634] Handle fingerprints as bytes instead of hex --- hwilib/commands.py | 9 ++++----- hwilib/devices/digitalbitbox.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index 01d0ba54b..56c2af72a 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -2,7 +2,6 @@ # Hardware wallet interaction script -import binascii import importlib import platform @@ -140,7 +139,7 @@ def getkeypool_inner( account: int = 0, addr_type: AddressType = AddressType.WPKH ) -> List[Dict[str, Any]]: - master_fpr = client.get_master_fingerprint().hex() + master_fpr = client.get_master_fingerprint() desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) @@ -160,7 +159,7 @@ def getkeypool_inner( def getdescriptor( client: HardwareWalletClient, - master_fpr: str, + master_fpr: bytes, path: Optional[str] = None, internal: bool = False, addr_type: AddressType = AddressType.WPKH, @@ -209,7 +208,7 @@ def getdescriptor( break i += 1 - origin = KeyOriginInfo(binascii.unhexlify(master_fpr), parsed_path[:i]) + origin = KeyOriginInfo(master_fpr, parsed_path[:i]) path_base = origin.get_derivation_path() path_suffix = "" @@ -263,7 +262,7 @@ def getdescriptors( client: HardwareWalletClient, account: int = 0 ) -> Dict[str, List[str]]: - master_fpr = client.get_master_fingerprint().hex() + master_fpr = client.get_master_fingerprint() result = {} diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 831d72bb8..1bcfe4cc8 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -374,7 +374,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: blank_tx = CTransaction(tx.tx) # Get the master key fingerprint - master_fp = self.get_master_fingerprint().hex() + master_fp = self.get_master_fingerprint() # create sighashes sighash_tuples = [] @@ -464,7 +464,7 @@ def sign_tx(self, tx: PSBT) -> PSBT: # Figure out which keypath thing is for this input for pubkey, keypath in psbt_in.hd_keypaths.items(): - if master_fp == keypath.fingerprint.hex(): + if master_fp == keypath.fingerprint: # Add the keypath strings keypath_str = keypath.get_derivation_path() From 41740d3c7c21b1e545027f05ef35da9ba8b80e64 Mon Sep 17 00:00:00 2001 From: Dimitri Date: Sun, 21 Feb 2021 16:29:40 +0100 Subject: [PATCH 313/634] Doc: Change the `getkeypool`'s obsolete `--wpkh`s to `--addr-type wpkh` --- docs/bitcoin-core-usage.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/bitcoin-core-usage.md b/docs/bitcoin-core-usage.md index 2d90e9256..4c92d6bb2 100644 --- a/docs/bitcoin-core-usage.md +++ b/docs/bitcoin-core-usage.md @@ -47,7 +47,7 @@ We will be fetching keys at the BIP 84 default. If `--path` and `--internal` are specified, both receiving and change address descriptors are generated. ``` -$ ./hwi.py -f 8038ecd9 getkeypool --wpkh 0 1000 +$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 0 1000 [{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}] ``` @@ -276,8 +276,8 @@ e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf When the keypools run out, they can be refilled by using the `getkeypool` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following `getkeypool` commands: ``` -$ ./hwi.py -f 8038ecd9 getkeypool --wpkh 1000 2000 -$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --internal 1000 2000 +$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 1000 2000 +$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh --internal 1000 2000 ``` The output can be imported with `importmulti` as shown in the Setup steps. @@ -285,8 +285,8 @@ The output can be imported with `importmulti` as shown in the Setup steps. The instructions above use BIP 84 to derive keys used for P2WPKH addresses (bech32 addresses). HWI follows BIPs 44, 84, and 49. By default, descriptors will be for P2PKH addresses with keys derived at `m/44h/0h/0h/0` for normal receiving keys and `m/44h/0h/0h/1` for change keys. -Using the `--wpkh` option will result in P2WPKH addresses with keys derived at `m/84h/0h/0h/0` for normal receiving keys and `m/84h/0h/0h/1` for change keys. -Using the `sh_wpkh` option will result in P2SH nested P2WPKH addresses with keys derived at `m/49h/0h/0h/0` for normal receiving keys and `m/49h/0h/0h/1` for change keys. +Using the `--addr-type wpkh` option will result in P2WPKH addresses with keys derived at `m/84h/0h/0h/0` for normal receiving keys and `m/84h/0h/0h/1` for change keys. +Using the `--addr-type sh_wpkh` option will result in P2SH nested P2WPKH addresses with keys derived at `m/49h/0h/0h/0` for normal receiving keys and `m/49h/0h/0h/1` for change keys. To actually get the correct address type when using `getnewaddress` from Bitcoin Core, you will need to additionally set `-addresstype=p2sh-segwit` and `-changetype=p2sh-segwit`. This can be set in the command line (as shown in the example) or in your bitcoin.conf file. From 40c6887af5816e61ab6856af5099f3898c63eacc Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Mon, 22 Feb 2021 18:12:47 +0100 Subject: [PATCH 314/634] make more distinction between Yes, N/A and No in the readme table by using checkmarkm, dash and cross --- README.md | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 059b130dc..4b0168d40 100644 --- a/README.md +++ b/README.md @@ -90,29 +90,33 @@ The below table lists what devices and features are supported for each device. Please also see [docs](docs/) for additional information about each device. +* `✓` - supported by the firmware and implemented in HWI +* `✗` - supported by the firmware and not implemented in HWI +* `-` - not supported by the firmware + | Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard | |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | -| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | -| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A | -| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A | -| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes | -| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | -| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | -| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | -| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | -| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A | -| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | -| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes | -| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes | +| Support Planned | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Implemented | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| xpub retrieval | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Message Signing | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | +| Device Setup | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| Device Wipe | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| Device Recovery | - | - | ✓ | ✓ | - | ✓ | ✓ | - | +| Device Backup | - | - | - | - | ✓ | ✓ | - | ✓ | +| P2PKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | +| P2SH-P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| P2SH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| P2SH-P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Bare Multisig Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | +| Arbitrary scriptPubKey Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | +| Arbitrary redeemScript Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | +| Arbitrary witnessScript Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | +| Non-wallet inputs | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | +| Mixed Segwit and Non-Segwit Inputs | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Display on device screen | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | ✓ | ## Using with Bitcoin Core From 62fea1fbac95569de903d9f0aab4ef2092d19a3a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 16:15:27 -0500 Subject: [PATCH 315/634] Move AddressType to common.py --- hwilib/_cli.py | 6 ++++-- hwilib/_gui.py | 3 +-- hwilib/commands.py | 7 +++++-- hwilib/common.py | 19 +++++++++++++++++++ hwilib/devices/bitbox02.py | 6 ++++-- hwilib/devices/coldcard.py | 6 ++++-- hwilib/devices/digitalbitbox.py | 6 ++++-- hwilib/devices/ledger.py | 6 ++++-- hwilib/devices/trezor.py | 6 ++++-- hwilib/hwwclient.py | 4 ++-- hwilib/serializations.py | 20 -------------------- 11 files changed, 51 insertions(+), 38 deletions(-) diff --git a/hwilib/_cli.py b/hwilib/_cli.py index effc04082..2073d8b6e 100644 --- a/hwilib/_cli.py +++ b/hwilib/_cli.py @@ -20,7 +20,10 @@ wipe_device, install_udev_rules, ) -from .common import Chain +from .common import ( + AddressType, + Chain, +) from .errors import ( handle_errors, DEVICE_CONN_ERROR, @@ -31,7 +34,6 @@ UNKNOWN_ERROR, ) from .hwwclient import HardwareWalletClient -from .serializations import AddressType from . import __version__ import argparse diff --git a/hwilib/_gui.py b/hwilib/_gui.py index 186a2eee0..a8f6b5d21 100644 --- a/hwilib/_gui.py +++ b/hwilib/_gui.py @@ -9,8 +9,7 @@ from . import commands, __version__ from ._cli import HWIArgumentParser from .errors import handle_errors, DEVICE_NOT_INITIALIZED -from .serializations import AddressType -from .common import Chain +from .common import AddressType, Chain try: from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog diff --git a/hwilib/commands.py b/hwilib/commands.py index 8569f7cf0..28128ee7e 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -5,7 +5,7 @@ import importlib import platform -from .serializations import AddressType, PSBT +from .serializations import PSBT from ._base58 import xpub_to_pub_hex from .key import ( H_, @@ -31,7 +31,10 @@ WSHDescriptor, ) from .devices import __all__ as all_devs -from .common import Chain +from .common import ( + AddressType, + Chain, +) from .hwwclient import HardwareWalletClient from itertools import count diff --git a/hwilib/common.py b/hwilib/common.py index a0a1f0fe3..7af960bfe 100644 --- a/hwilib/common.py +++ b/hwilib/common.py @@ -21,3 +21,22 @@ def argparse(s: str) -> Union['Chain', str]: return Chain[s.upper()] except KeyError: return s + + +class AddressType(Enum): + PKH = 1 + WPKH = 2 + SH_WPKH = 3 + + def __str__(self) -> str: + return self.name.lower() + + def __repr__(self) -> str: + return str(self) + + @staticmethod + def argparse(s: str) -> Union['AddressType', str]: + try: + return AddressType[s.upper()] + except KeyError: + return s diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index cba39b3e3..f38793d0a 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -21,7 +21,6 @@ from ..hwwclient import HardwareWalletClient from ..key import ExtendedKey from ..serializations import ( - AddressType, PSBT, CTxOut, is_p2pkh, @@ -45,7 +44,10 @@ KeyOriginInfo, parse_path, ) -from ..common import Chain +from ..common import ( + AddressType, + Chain, +) import hid diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 3a30985a6..bf512566f 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -44,10 +44,12 @@ ExtendedKey, ) from ..serializations import ( - AddressType, PSBT, ) -from ..common import Chain +from ..common import ( + AddressType, + Chain, +) from hashlib import sha256 import base64 diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 1bcfe4cc8..d534e1865 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -22,6 +22,10 @@ Union, ) +from ..common import ( + AddressType, + Chain, +) from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..errors import ( @@ -40,7 +44,6 @@ ExtendedKey, ) from ..serializations import ( - AddressType, CTransaction, hash256, is_p2pk, @@ -55,7 +58,6 @@ ser_string, ser_compact_size, ) -from ..common import Chain applen = 225280 # flash size minus bootloader length chunksize = 8 * 512 diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 43b994927..2790939da 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -20,7 +20,10 @@ common_err_msgs, handle_errors, ) -from ..common import Chain +from ..common import ( + AddressType, + Chain, +) from .btchip.bitcoinTransaction import bitcoinTransaction from .btchip.btchip import btchip from .btchip.btchipComm import ( @@ -38,7 +41,6 @@ parse_path, ) from ..serializations import ( - AddressType, CTransaction, hash160, is_p2sh, diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 8c84acbc7..e1895a817 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -50,7 +50,6 @@ parse_path, ) from ..serializations import ( - AddressType, CTxOut, is_p2pkh, is_p2sh, @@ -59,7 +58,10 @@ PSBT, ser_uint256, ) -from ..common import Chain +from ..common import ( + AddressType, + Chain, +) from .. import _bech32 as bech32 from mnemonic import Mnemonic from usb1 import USBErrorNoDevice diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 7d7d0cf50..5f7da8662 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -6,8 +6,8 @@ ) from .descriptor import PubkeyProvider from .key import ExtendedKey -from .serializations import AddressType, PSBT -from .common import Chain +from .serializations import PSBT +from .common import AddressType, Chain class HardwareWalletClient(object): diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 838078127..d7c41f815 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -24,7 +24,6 @@ import copy import base64 -from enum import Enum from io import BytesIO, BufferedReader from typing import ( Dict, @@ -35,7 +34,6 @@ Sequence, Tuple, TypeVar, - Union, Callable, ) from typing_extensions import Protocol @@ -65,24 +63,6 @@ def hash160(s: bytes) -> bytes: return ripemd160(sha256(s)) -class AddressType(Enum): - PKH = 1 - WPKH = 2 - SH_WPKH = 3 - - def __str__(self) -> str: - return self.name.lower() - - def __repr__(self) -> str: - return str(self) - - @staticmethod - def argparse(s: str) -> Union['AddressType', str]: - try: - return AddressType[s.upper()] - except KeyError: - return s - # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: r = b"" From 3f41814ce8a17b78f1875f61201f659c09d50a81 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 16:16:49 -0500 Subject: [PATCH 316/634] Move hashers to common.py --- .cirrus.yml | 1 + hwilib/common.py | 18 ++++++++++++++++++ hwilib/descriptor.py | 2 +- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/ledger.py | 2 +- hwilib/serializations.py | 16 +++------------- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 7a15c99a6..91701ee63 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -134,6 +134,7 @@ basic_test_task: hwilib/_bech32.py hwilib/_cli.py hwilib/commands.py + hwilib/common.py hwilib/descriptor.py hwilib/devices/bitbox02.py hwilib/devices/coldcard.py diff --git a/hwilib/common.py b/hwilib/common.py index 7af960bfe..1da2a57a5 100644 --- a/hwilib/common.py +++ b/hwilib/common.py @@ -1,3 +1,5 @@ +import hashlib + from enum import Enum from typing import Union @@ -40,3 +42,19 @@ def argparse(s: str) -> Union['AddressType', str]: return AddressType[s.upper()] except KeyError: return s + + +def sha256(s: bytes) -> bytes: + return hashlib.new('sha256', s).digest() + + +def ripemd160(s: bytes) -> bytes: + return hashlib.new('ripemd160', s).digest() + + +def hash256(s: bytes) -> bytes: + return sha256(sha256(s)) + + +def hash160(s: bytes) -> bytes: + return ripemd160(sha256(s)) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index 7352e2aef..dd35b6b91 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,5 +1,5 @@ from .key import ExtendedKey, KeyOriginInfo, parse_path -from .serializations import hash160, sha256 +from .common import hash160, sha256 from binascii import unhexlify from collections import namedtuple diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index d534e1865..19dff76fc 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -25,6 +25,7 @@ from ..common import ( AddressType, Chain, + hash256, ) from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient @@ -45,7 +46,6 @@ ) from ..serializations import ( CTransaction, - hash256, is_p2pk, is_p2pkh, is_p2sh, diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 2790939da..49329f94d 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -23,6 +23,7 @@ from ..common import ( AddressType, Chain, + hash160, ) from .btchip.bitcoinTransaction import bitcoinTransaction from .btchip.btchip import btchip @@ -42,7 +43,6 @@ ) from ..serializations import ( CTransaction, - hash160, is_p2sh, is_p2wpkh, is_p2wsh, diff --git a/hwilib/serializations.py b/hwilib/serializations.py index d7c41f815..c8fc04f3d 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -15,12 +15,14 @@ ser_*, deser_*: functions that handle serialization/deserialization """ +from .common import ( + hash256, +) from .errors import PSBTSerializationError from .key import KeyOriginInfo import struct import binascii -import hashlib import copy import base64 @@ -50,18 +52,6 @@ class Serializable(Protocol): def serialize(self) -> bytes: ... -def sha256(s: bytes) -> bytes: - return hashlib.new('sha256', s).digest() - -def ripemd160(s: bytes) -> bytes: - return hashlib.new('ripemd160', s).digest() - -def hash256(s: bytes) -> bytes: - return sha256(sha256(s)) - -def hash160(s: bytes) -> bytes: - return ripemd160(sha256(s)) - # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: From 538b4704b0a7d10b9827bfac5f900c12803be90b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 16:59:23 -0500 Subject: [PATCH 317/634] Unify hashers to use common hashers --- hwilib/_base58.py | 8 +------- hwilib/devices/digitalbitbox.py | 5 +---- hwilib/key.py | 8 ++++++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/hwilib/_base58.py b/hwilib/_base58.py index b9f604562..9df34be14 100644 --- a/hwilib/_base58.py +++ b/hwilib/_base58.py @@ -8,21 +8,15 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. # -import hashlib - from binascii import hexlify, unhexlify from typing import List +from .common import hash256 from .errors import BadArgumentError b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -def sha256(s: bytes) -> bytes: - return hashlib.new('sha256', s).digest() - -def hash256(s: bytes) -> bytes: - return sha256(sha256(s)) def encode(b: bytes) -> str: """Encode bytes to a base58-encoded string""" diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 19dff76fc..93b271344 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -189,16 +189,13 @@ def decrypt_aes(secret: bytes, e: bytes) -> bytes: s = aes_decrypt_with_iv(secret, iv, e) return s -def sha256(x: bytes) -> bytes: - return hashlib.sha256(x).digest() - def sha512(x: bytes) -> bytes: return hashlib.sha512(x).digest() def double_hash(x: Union[str, bytes]) -> bytes: if not isinstance(x, bytes): x = x.encode('utf-8') - return sha256(sha256(x)) + return hash256(x) def derive_keys(x: str) -> Tuple[bytes, bytes]: h = double_hash(x) diff --git a/hwilib/key.py b/hwilib/key.py index 515c06d1a..73c7fbddf 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -4,6 +4,10 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. from . import _base58 as base58 +from .common import ( + hash256, + hash160, +) from .errors import BadArgumentError import binascii @@ -139,7 +143,7 @@ def serialize(self) -> bytes: def to_string(self) -> str: data = self.serialize() - checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()[0:4] + checksum = hash256(data)[0:4] return base58.encode(data + checksum) def get_printable_dict(self) -> Dict[str, object]: @@ -174,7 +178,7 @@ def derive_pub(self, i: int) -> 'ExtendedKey': # Construct and return a new BIP32Key pubkey = point_to_bytes(child_pubkey) chaincode = Ir - fingerprint = hashlib.new('ripemd160', hashlib.sha256(self.pubkey).digest()).digest()[0:4] + fingerprint = hash160(self.pubkey)[0:4] return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth + 1, fingerprint, i, chaincode, None, pubkey) def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey': From 47c7648154af9b1d61296c41a15854d2337da947 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 18:03:18 -0500 Subject: [PATCH 318/634] Move script type detection to its own module --- .cirrus.yml | 2 + hwilib/_script.py | 83 +++++++++++++++++++++++++++++++++ hwilib/devices/bitbox02.py | 10 ++-- hwilib/devices/digitalbitbox.py | 6 ++- hwilib/devices/ledger.py | 6 ++- hwilib/devices/trezor.py | 6 ++- hwilib/serializations.py | 78 ++++--------------------------- 7 files changed, 111 insertions(+), 80 deletions(-) create mode 100644 hwilib/_script.py diff --git a/.cirrus.yml b/.cirrus.yml index 91701ee63..7a3a9d2f1 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -144,6 +144,8 @@ basic_test_task: hwilib/devices/ledger.py hwilib/devices/trezor.py hwilib/errors.py + hwilib/_script.py + hwilib/serializations.py hwilib/hwwclient.py hwilib/__init__.py hwilib/key.py diff --git a/hwilib/_script.py b/hwilib/_script.py new file mode 100644 index 000000000..6f30ce758 --- /dev/null +++ b/hwilib/_script.py @@ -0,0 +1,83 @@ +from typing import ( + Optional, + Sequence, + Tuple, +) + + +def is_opreturn(script: bytes) -> bool: + return script[0] == 0x6a + + +def is_p2sh(script: bytes) -> bool: + return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 + + +def is_p2pkh(script: bytes) -> bool: + return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac + + +def is_p2pk(script: bytes) -> bool: + return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac + + +def is_witness(script: bytes) -> Tuple[bool, int, bytes]: + if len(script) < 4 or len(script) > 42: + return (False, 0, b"") + + if script[0] != 0 and (script[0] < 81 or script[0] > 96): + return (False, 0, b"") + + if script[1] + 2 == len(script): + return (True, script[0] - 0x50 if script[0] else 0, script[2:]) + + return (False, 0, b"") + + +def is_p2wpkh(script: bytes) -> bool: + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 20 + + +def is_p2wsh(script: bytes) -> bool: + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 32 + + +# Only handles up to 15 of 15. Returns None if this script is not a +# multisig script. Returns (m, pubkeys) otherwise. +def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: + # Get m + m = script[0] - 80 + if m < 1 or m > 15: + return None + + # Get pubkeys + pubkeys = [] + offset = 1 + while True: + pubkey_len = script[offset] + if pubkey_len != 33: + break + offset += 1 + pubkeys.append(script[offset:offset + 33]) + offset += 33 + + # Check things at the end + n = script[offset] - 80 + if n != len(pubkeys): + return None + offset += 1 + op_cms = script[offset] + if op_cms != 174: + return None + + return (m, pubkeys) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index f38793d0a..39a15a48c 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -20,15 +20,17 @@ from ..descriptor import PubkeyProvider from ..hwwclient import HardwareWalletClient from ..key import ExtendedKey -from ..serializations import ( - PSBT, - CTxOut, +from .._script import ( is_p2pkh, is_p2wpkh, is_p2wsh, + parse_multisig, +) +from ..serializations import ( + PSBT, + CTxOut, ser_uint256, ser_sig_der, - parse_multisig, ) from ..errors import ( HWWError, diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 93b271344..39f4f04eb 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -44,14 +44,16 @@ from ..key import ( ExtendedKey, ) -from ..serializations import ( - CTransaction, +from .._script import ( is_p2pk, is_p2pkh, is_p2sh, is_p2wpkh, is_p2wsh, is_witness, +) +from ..serializations import ( + CTransaction, PSBT, ser_sig_der, ser_sig_compact, diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 49329f94d..052c67348 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -41,12 +41,14 @@ ExtendedKey, parse_path, ) -from ..serializations import ( - CTransaction, +from .._script import ( is_p2sh, is_p2wpkh, is_p2wsh, is_witness, +) +from ..serializations import ( + CTransaction, PSBT, ) import logging diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index e1895a817..18b99eb56 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -49,12 +49,14 @@ ExtendedKey, parse_path, ) -from ..serializations import ( - CTxOut, +from .._script import ( is_p2pkh, is_p2sh, is_p2wsh, is_witness, +) +from ..serializations import ( + CTxOut, PSBT, ser_uint256, ) diff --git a/hwilib/serializations.py b/hwilib/serializations.py index c8fc04f3d..6db076fc4 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -20,6 +20,14 @@ ) from .errors import PSBTSerializationError from .key import KeyOriginInfo +from ._script import ( + is_opreturn, + is_p2sh, + is_p2pkh, + is_p2pk, + is_witness, + is_p2wsh, +) import struct import binascii @@ -253,76 +261,6 @@ def __repr__(self) -> str: self.nSequence) -def is_opreturn(script: bytes) -> bool: - return script[0] == 0x6a - -def is_p2sh(script: bytes) -> bool: - return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 - -def is_p2pkh(script: bytes) -> bool: - return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac - -def is_p2pk(script: bytes) -> bool: - return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac - -def is_witness(script: bytes) -> Tuple[bool, int, bytes]: - if len(script) < 4 or len(script) > 42: - return (False, 0, b"") - - if script[0] != 0 and (script[0] < 81 or script[0] > 96): - return (False, 0, b"") - - if script[1] + 2 == len(script): - return (True, script[0] - 0x50 if script[0] else 0, script[2:]) - - return (False, 0, b"") - -def is_p2wpkh(script: bytes) -> bool: - is_wit, wit_ver, wit_prog = is_witness(script) - if not is_wit: - return False - elif wit_ver != 0: - return False - return len(wit_prog) == 20 - -def is_p2wsh(script: bytes) -> bool: - is_wit, wit_ver, wit_prog = is_witness(script) - if not is_wit: - return False - elif wit_ver != 0: - return False - return len(wit_prog) == 32 - -# Only handles up to 15 of 15. Returns None if this script is not a -# multisig script. Returns (m, pubkeys) otherwise. -def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: - # Get m - m = script[0] - 80 - if m < 1 or m > 15: - return None - - # Get pubkeys - pubkeys = [] - offset = 1 - while True: - pubkey_len = script[offset] - if pubkey_len != 33: - break - offset += 1 - pubkeys.append(script[offset:offset + 33]) - offset += 33 - - # Check things at the end - n = script[offset] - 80 - if n != len(pubkeys): - return None - offset += 1 - op_cms = script[offset] - if op_cms != 174: - return None - - return (m, pubkeys) - class CTxOut(object): def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): self.nValue = nValue From 4de2295e57e66e4bf44e6741d3b99cb5db67d73e Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 19:06:49 -0500 Subject: [PATCH 319/634] Move PSBT stuff to its own module --- hwilib/commands.py | 2 +- hwilib/devices/bitbox02.py | 2 +- hwilib/devices/coldcard.py | 2 +- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/ledger.py | 2 +- hwilib/devices/trezor.py | 2 +- hwilib/hwwclient.py | 2 +- hwilib/psbt.py | 409 ++++++++++++++++++++++++++++++++ hwilib/serializations.py | 396 ------------------------------- test/test_device.py | 2 +- test/test_psbt.py | 2 +- 11 files changed, 418 insertions(+), 405 deletions(-) create mode 100644 hwilib/psbt.py diff --git a/hwilib/commands.py b/hwilib/commands.py index 28128ee7e..ce25c3ebd 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -5,7 +5,6 @@ import importlib import platform -from .serializations import PSBT from ._base58 import xpub_to_pub_hex from .key import ( H_, @@ -36,6 +35,7 @@ Chain, ) from .hwwclient import HardwareWalletClient +from .psbt import PSBT from itertools import count from typing import ( diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 39a15a48c..26f889b92 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -26,8 +26,8 @@ is_p2wsh, parse_multisig, ) +from ..psbt import PSBT from ..serializations import ( - PSBT, CTxOut, ser_uint256, ser_sig_der, diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index bf512566f..18e0cd120 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -43,7 +43,7 @@ from ..key import ( ExtendedKey, ) -from ..serializations import ( +from ..psbt import ( PSBT, ) from ..common import ( diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 39f4f04eb..12b906bce 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -52,9 +52,9 @@ is_p2wsh, is_witness, ) +from ..psbt import PSBT from ..serializations import ( CTransaction, - PSBT, ser_sig_der, ser_sig_compact, ser_string, diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 052c67348..0b45b273c 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -47,9 +47,9 @@ is_p2wsh, is_witness, ) +from ..psbt import PSBT from ..serializations import ( CTransaction, - PSBT, ) import logging import re diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 18b99eb56..00b923901 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -55,9 +55,9 @@ is_p2wsh, is_witness, ) +from ..psbt import PSBT from ..serializations import ( CTxOut, - PSBT, ser_uint256, ) from ..common import ( diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 5f7da8662..e551e65c4 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -6,7 +6,7 @@ ) from .descriptor import PubkeyProvider from .key import ExtendedKey -from .serializations import PSBT +from .psbt import PSBT from .common import AddressType, Chain diff --git a/hwilib/psbt.py b/hwilib/psbt.py new file mode 100644 index 000000000..8a8227e38 --- /dev/null +++ b/hwilib/psbt.py @@ -0,0 +1,409 @@ +import base64 +import struct + +from io import BytesIO, BufferedReader +from typing import ( + Dict, + List, + Mapping, + MutableMapping, + Optional, + Sequence, +) + +from .key import KeyOriginInfo +from .errors import PSBTSerializationError +from .serializations import ( + CTransaction, + CTxInWitness, + CTxOut, + deser_string, + Readable, + ser_compact_size, + ser_string, +) + +def DeserializeHDKeypath( + f: Readable, + key: bytes, + hd_keypaths: MutableMapping[bytes, KeyOriginInfo], + expected_sizes: Sequence[int], +) -> None: + if len(key) not in expected_sizes: + raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey. Length: {}".format(len(key))) + pubkey = key[1:] + if pubkey in hd_keypaths: + raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") + + hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) + +def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) -> bytes: + r = b"" + for pubkey, path in sorted(hd_keypaths.items()): + r += ser_string(type + pubkey) + packed = path.serialize() + r += ser_string(packed) + return r + +class PartiallySignedInput: + def __init__(self) -> None: + self.non_witness_utxo: Optional[CTransaction] = None + self.witness_utxo: Optional[CTxOut] = None + self.partial_sigs: Dict[bytes, bytes] = {} + self.sighash = 0 + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} + self.final_script_sig = b"" + self.final_script_witness = CTxInWitness() + self.unknown: Dict[bytes, bytes] = {} + + def set_null(self) -> None: + self.non_witness_utxo = None + self.witness_utxo = None + self.partial_sigs.clear() + self.sighash = 0 + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths.clear() + self.final_script_sig = b"" + self.final_script_witness = CTxInWitness() + self.unknown.clear() + + def deserialize(self, f: Readable) -> None: + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = struct.unpack("b", bytearray([key[0]]))[0] + + if key_type == 0: + if self.non_witness_utxo: + raise PSBTSerializationError("Duplicate Key, input non witness utxo already provided") + elif len(key) != 1: + raise PSBTSerializationError("non witness utxo key is more than one byte type") + self.non_witness_utxo = CTransaction() + utxo_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.non_witness_utxo.deserialize(utxo_bytes) + self.non_witness_utxo.rehash() + + elif key_type == 1: + if self.witness_utxo: + raise PSBTSerializationError("Duplicate Key, input witness utxo already provided") + elif len(key) != 1: + raise PSBTSerializationError("witness utxo key is more than one byte type") + self.witness_utxo = CTxOut() + tx_out_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.witness_utxo.deserialize(tx_out_bytes) + + elif key_type == 2: + if len(key) != 34 and len(key) != 66: + raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") + pubkey = key[1:] + if pubkey in self.partial_sigs: + raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") + + sig = deser_string(f) + self.partial_sigs[pubkey] = sig + + elif key_type == 3: + if self.sighash > 0: + raise PSBTSerializationError("Duplicate key, input sighash type already provided") + elif len(key) != 1: + raise PSBTSerializationError("sighash key is more than one byte type") + sighash_bytes = deser_string(f) + self.sighash = struct.unpack(" bytes: + r = b"" + + if self.non_witness_utxo: + r += ser_string(b"\x00") + tx = self.non_witness_utxo.serialize_with_witness() + r += ser_string(tx) + + if self.witness_utxo: + r += ser_string(b"\x01") + tx = self.witness_utxo.serialize() + r += ser_string(tx) + + if len(self.final_script_sig) == 0 and self.final_script_witness.is_null(): + for pubkey, sig in sorted(self.partial_sigs.items()): + r += ser_string(b"\x02" + pubkey) + r += ser_string(sig) + + if self.sighash > 0: + r += ser_string(b"\x03") + r += ser_string(struct.pack(" None: + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} + self.unknown: Dict[bytes, bytes] = {} + + def set_null(self) -> None: + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths.clear() + self.unknown.clear() + + def deserialize(self, f: Readable) -> None: + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = struct.unpack("b", bytearray([key[0]]))[0] + + if key_type == 0: + if len(self.redeem_script) != 0: + raise PSBTSerializationError("Duplicate key, output redeemScript already provided") + elif len(key) != 1: + raise PSBTSerializationError("Output redeemScript key is more than one byte type") + self.redeem_script = deser_string(f) + + elif key_type == 1: + if len(self.witness_script) != 0: + raise PSBTSerializationError("Duplicate key, output witnessScript already provided") + elif len(key) != 1: + raise PSBTSerializationError("Output witnessScript key is more than one byte type") + self.witness_script = deser_string(f) + + elif key_type == 2: + DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66]) + + else: + if key in self.unknown: + raise PSBTSerializationError("Duplicate key, key for unknown value already provided") + value = deser_string(f) + self.unknown[key] = value + + def serialize(self) -> bytes: + r = b"" + if len(self.redeem_script) != 0: + r += ser_string(b"\x00") + r += ser_string(self.redeem_script) + + if len(self.witness_script) != 0: + r += ser_string(b"\x01") + r += ser_string(self.witness_script) + + r += SerializeHDKeypath(self.hd_keypaths, b"\x02") + + for key, value in sorted(self.unknown.items()): + r += ser_string(key) + r += ser_string(value) + + r += b"\x00" + + return r + +class PSBT(object): + + def __init__(self, tx: Optional[CTransaction] = None) -> None: + if tx: + self.tx = tx + else: + self.tx = CTransaction() + self.inputs: List[PartiallySignedInput] = [] + self.outputs: List[PartiallySignedOutput] = [] + self.unknown: Dict[bytes, bytes] = {} + self.xpub: Dict[bytes, KeyOriginInfo] = {} + + def deserialize(self, psbt: str) -> None: + psbt_bytes = base64.b64decode(psbt.strip()) + f = BufferedReader(BytesIO(psbt_bytes)) # type: ignore + end = len(psbt_bytes) + + # Read the magic bytes + magic = f.read(5) + if magic != b"psbt\xff": + raise PSBTSerializationError("invalid magic") + + # Read loop + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = struct.unpack("b", bytearray([key[0]]))[0] + + # Do stuff based on type + if key_type == 0x00: + # Checks for correctness + if not self.tx.is_null: + raise PSBTSerializationError("Duplicate key, unsigned tx already provided") + elif len(key) > 1: + raise PSBTSerializationError("Global unsigned tx key is more than one byte type") + + # read in value + tx_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore + self.tx.deserialize(tx_bytes) + + # Make sure that all scriptSigs and scriptWitnesses are empty + for txin in self.tx.vin: + if len(txin.scriptSig) != 0 or not self.tx.wit.is_null(): + raise PSBTSerializationError("Unsigned tx does not have empty scriptSigs and scriptWitnesses") + elif key_type == 0x01: + DeserializeHDKeypath(f, key, self.xpub, [79]) + else: + if key in self.unknown: + raise PSBTSerializationError("Duplicate key, key for unknown value already provided") + unknown_bytes = deser_string(f) + self.unknown[key] = unknown_bytes + + # make sure that we got an unsigned tx + if self.tx.is_null(): + raise PSBTSerializationError("No unsigned trasaction was provided") + + # Read input data + for txin in self.tx.vin: + if f.tell() == end: + break + input = PartiallySignedInput() + input.deserialize(f) + self.inputs.append(input) + + if input.non_witness_utxo: + input.non_witness_utxo.rehash() + if input.non_witness_utxo.sha256 != txin.prevout.hash: + raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash") + + if (len(self.inputs) != len(self.tx.vin)): + raise PSBTSerializationError("Inputs provided does not match the number of inputs in transaction") + + # Read output data + for txout in self.tx.vout: + if f.tell() == end: + break + output = PartiallySignedOutput() + output.deserialize(f) + self.outputs.append(output) + + if len(self.outputs) != len(self.tx.vout): + raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") + + def serialize(self) -> str: + r = b"" + + # magic bytes + r += b"psbt\xff" + + # unsigned tx flag + r += b"\x01\x00" + + # write serialized tx + tx = self.tx.serialize_with_witness() + r += ser_compact_size(len(tx)) + r += tx + + # write xpubs + r += SerializeHDKeypath(self.xpub, b"\x01") + + # unknowns + for key, value in sorted(self.unknown.items()): + r += ser_string(key) + r += ser_string(value) + + # separator + r += b"\x00" + + # inputs + for input in self.inputs: + r += input.serialize() + + # outputs + for output in self.outputs: + r += output.serialize() + + # return hex string + return base64.b64encode(r).decode() diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 6db076fc4..992ccf032 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -18,8 +18,6 @@ from .common import ( hash256, ) -from .errors import PSBTSerializationError -from .key import KeyOriginInfo from ._script import ( is_opreturn, is_p2sh, @@ -30,16 +28,10 @@ ) import struct -import binascii import copy -import base64 -from io import BytesIO, BufferedReader from typing import ( - Dict, List, - Mapping, - MutableMapping, Optional, Sequence, Tuple, @@ -148,9 +140,6 @@ def ser_string_vector(v: List[bytes]) -> bytes: r += ser_string(sv) return r -def HexToBase64(s: str) -> bytes: - return base64.b64encode(binascii.unhexlify(s)) - def ser_sig_der(r: bytes, s: bytes) -> bytes: sig = b"\x30" @@ -456,388 +445,3 @@ def is_null(self) -> bool: def __repr__(self) -> str: return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) - -def DeserializeHDKeypath( - f: Readable, - key: bytes, - hd_keypaths: MutableMapping[bytes, KeyOriginInfo], - expected_sizes: Sequence[int], -) -> None: - if len(key) not in expected_sizes: - raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey. Length: {}".format(len(key))) - pubkey = key[1:] - if pubkey in hd_keypaths: - raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") - - hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) - -def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) -> bytes: - r = b"" - for pubkey, path in sorted(hd_keypaths.items()): - r += ser_string(type + pubkey) - packed = path.serialize() - r += ser_string(packed) - return r - -class PartiallySignedInput: - def __init__(self) -> None: - self.non_witness_utxo: Optional[CTransaction] = None - self.witness_utxo: Optional[CTxOut] = None - self.partial_sigs: Dict[bytes, bytes] = {} - self.sighash = 0 - self.redeem_script = b"" - self.witness_script = b"" - self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} - self.final_script_sig = b"" - self.final_script_witness = CTxInWitness() - self.unknown: Dict[bytes, bytes] = {} - - def set_null(self) -> None: - self.non_witness_utxo = None - self.witness_utxo = None - self.partial_sigs.clear() - self.sighash = 0 - self.redeem_script = b"" - self.witness_script = b"" - self.hd_keypaths.clear() - self.final_script_sig = b"" - self.final_script_witness = CTxInWitness() - self.unknown.clear() - - def deserialize(self, f: Readable) -> None: - while True: - # read the key - try: - key = deser_string(f) - except Exception: - break - - # Check for separator - if len(key) == 0: - break - - # First byte of key is the type - key_type = struct.unpack("b", bytearray([key[0]]))[0] - - if key_type == 0: - if self.non_witness_utxo: - raise PSBTSerializationError("Duplicate Key, input non witness utxo already provided") - elif len(key) != 1: - raise PSBTSerializationError("non witness utxo key is more than one byte type") - self.non_witness_utxo = CTransaction() - utxo_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore - self.non_witness_utxo.deserialize(utxo_bytes) - self.non_witness_utxo.rehash() - - elif key_type == 1: - if self.witness_utxo: - raise PSBTSerializationError("Duplicate Key, input witness utxo already provided") - elif len(key) != 1: - raise PSBTSerializationError("witness utxo key is more than one byte type") - self.witness_utxo = CTxOut() - tx_out_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore - self.witness_utxo.deserialize(tx_out_bytes) - - elif key_type == 2: - if len(key) != 34 and len(key) != 66: - raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") - pubkey = key[1:] - if pubkey in self.partial_sigs: - raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") - - sig = deser_string(f) - self.partial_sigs[pubkey] = sig - - elif key_type == 3: - if self.sighash > 0: - raise PSBTSerializationError("Duplicate key, input sighash type already provided") - elif len(key) != 1: - raise PSBTSerializationError("sighash key is more than one byte type") - sighash_bytes = deser_string(f) - self.sighash = struct.unpack(" bytes: - r = b"" - - if self.non_witness_utxo: - r += ser_string(b"\x00") - tx = self.non_witness_utxo.serialize_with_witness() - r += ser_string(tx) - - if self.witness_utxo: - r += ser_string(b"\x01") - tx = self.witness_utxo.serialize() - r += ser_string(tx) - - if len(self.final_script_sig) == 0 and self.final_script_witness.is_null(): - for pubkey, sig in sorted(self.partial_sigs.items()): - r += ser_string(b"\x02" + pubkey) - r += ser_string(sig) - - if self.sighash > 0: - r += ser_string(b"\x03") - r += ser_string(struct.pack(" None: - self.redeem_script = b"" - self.witness_script = b"" - self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} - self.unknown: Dict[bytes, bytes] = {} - - def set_null(self) -> None: - self.redeem_script = b"" - self.witness_script = b"" - self.hd_keypaths.clear() - self.unknown.clear() - - def deserialize(self, f: Readable) -> None: - while True: - # read the key - try: - key = deser_string(f) - except Exception: - break - - # Check for separator - if len(key) == 0: - break - - # First byte of key is the type - key_type = struct.unpack("b", bytearray([key[0]]))[0] - - if key_type == 0: - if len(self.redeem_script) != 0: - raise PSBTSerializationError("Duplicate key, output redeemScript already provided") - elif len(key) != 1: - raise PSBTSerializationError("Output redeemScript key is more than one byte type") - self.redeem_script = deser_string(f) - - elif key_type == 1: - if len(self.witness_script) != 0: - raise PSBTSerializationError("Duplicate key, output witnessScript already provided") - elif len(key) != 1: - raise PSBTSerializationError("Output witnessScript key is more than one byte type") - self.witness_script = deser_string(f) - - elif key_type == 2: - DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66]) - - else: - if key in self.unknown: - raise PSBTSerializationError("Duplicate key, key for unknown value already provided") - value = deser_string(f) - self.unknown[key] = value - - def serialize(self) -> bytes: - r = b"" - if len(self.redeem_script) != 0: - r += ser_string(b"\x00") - r += ser_string(self.redeem_script) - - if len(self.witness_script) != 0: - r += ser_string(b"\x01") - r += ser_string(self.witness_script) - - r += SerializeHDKeypath(self.hd_keypaths, b"\x02") - - for key, value in sorted(self.unknown.items()): - r += ser_string(key) - r += ser_string(value) - - r += b"\x00" - - return r - -class PSBT(object): - - def __init__(self, tx: Optional[CTransaction] = None) -> None: - if tx: - self.tx = tx - else: - self.tx = CTransaction() - self.inputs: List[PartiallySignedInput] = [] - self.outputs: List[PartiallySignedOutput] = [] - self.unknown: Dict[bytes, bytes] = {} - self.xpub: Dict[bytes, KeyOriginInfo] = {} - - def deserialize(self, psbt: str) -> None: - psbt_bytes = base64.b64decode(psbt.strip()) - f = BufferedReader(BytesIO(psbt_bytes)) # type: ignore - end = len(psbt_bytes) - - # Read the magic bytes - magic = f.read(5) - if magic != b"psbt\xff": - raise PSBTSerializationError("invalid magic") - - # Read loop - while True: - # read the key - try: - key = deser_string(f) - except Exception: - break - - # Check for separator - if len(key) == 0: - break - - # First byte of key is the type - key_type = struct.unpack("b", bytearray([key[0]]))[0] - - # Do stuff based on type - if key_type == 0x00: - # Checks for correctness - if not self.tx.is_null: - raise PSBTSerializationError("Duplicate key, unsigned tx already provided") - elif len(key) > 1: - raise PSBTSerializationError("Global unsigned tx key is more than one byte type") - - # read in value - tx_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore - self.tx.deserialize(tx_bytes) - - # Make sure that all scriptSigs and scriptWitnesses are empty - for txin in self.tx.vin: - if len(txin.scriptSig) != 0 or not self.tx.wit.is_null(): - raise PSBTSerializationError("Unsigned tx does not have empty scriptSigs and scriptWitnesses") - elif key_type == 0x01: - DeserializeHDKeypath(f, key, self.xpub, [79]) - else: - if key in self.unknown: - raise PSBTSerializationError("Duplicate key, key for unknown value already provided") - unknown_bytes = deser_string(f) - self.unknown[key] = unknown_bytes - - # make sure that we got an unsigned tx - if self.tx.is_null(): - raise PSBTSerializationError("No unsigned trasaction was provided") - - # Read input data - for txin in self.tx.vin: - if f.tell() == end: - break - input = PartiallySignedInput() - input.deserialize(f) - self.inputs.append(input) - - if input.non_witness_utxo: - input.non_witness_utxo.rehash() - if input.non_witness_utxo.sha256 != txin.prevout.hash: - raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash") - - if (len(self.inputs) != len(self.tx.vin)): - raise PSBTSerializationError("Inputs provided does not match the number of inputs in transaction") - - # Read output data - for txout in self.tx.vout: - if f.tell() == end: - break - output = PartiallySignedOutput() - output.deserialize(f) - self.outputs.append(output) - - if len(self.outputs) != len(self.tx.vout): - raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") - - def serialize(self) -> str: - r = b"" - - # magic bytes - r += b"psbt\xff" - - # unsigned tx flag - r += b"\x01\x00" - - # write serialized tx - tx = self.tx.serialize_with_witness() - r += ser_compact_size(len(tx)) - r += tx - - # write xpubs - r += SerializeHDKeypath(self.xpub, b"\x01") - - # unknowns - for key, value in sorted(self.unknown.items()): - r += ser_string(key) - r += ser_string(value) - - # separator - r += b"\x00" - - # inputs - for input in self.inputs: - r += input.serialize() - - # outputs - for output in self.outputs: - r += output.serialize() - - # return hex string - return HexToBase64(r.hex()).decode() diff --git a/test/test_device.py b/test/test_device.py index b4be090e1..c1d5cf48a 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -15,7 +15,7 @@ from hwilib._cli import process_commands from hwilib.descriptor import AddChecksum from hwilib.key import KeyOriginInfo -from hwilib.serializations import PSBT +from hwilib.psbt import PSBT SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'} SUPPORTS_XPUB_MS_DISPLAY = {'trezor_1', 'trezor_t'} diff --git a/test/test_psbt.py b/test/test_psbt.py index 7dacfa7c0..3a585b060 100755 --- a/test/test_psbt.py +++ b/test/test_psbt.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -from hwilib.serializations import PSBT +from hwilib.psbt import PSBT from hwilib.errors import PSBTSerializationError import json import os From 78c02d88ee89aeafa8a504f66366cc195748b481 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 18:35:53 -0500 Subject: [PATCH 320/634] Move CTransaction and related classes to separate module --- .cirrus.yml | 1 + hwilib/devices/bitbox02.py | 4 +- hwilib/devices/digitalbitbox.py | 4 +- hwilib/devices/ledger.py | 2 +- hwilib/devices/trezor.py | 4 +- hwilib/psbt.py | 4 +- hwilib/serializations.py | 270 ---------------------------- hwilib/tx.py | 300 ++++++++++++++++++++++++++++++++ 8 files changed, 314 insertions(+), 275 deletions(-) create mode 100644 hwilib/tx.py diff --git a/.cirrus.yml b/.cirrus.yml index 7a3a9d2f1..0514c50a7 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -146,6 +146,7 @@ basic_test_task: hwilib/errors.py hwilib/_script.py hwilib/serializations.py + hwilib/tx.py hwilib/hwwclient.py hwilib/__init__.py hwilib/key.py diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 26f889b92..7f6e4e5a4 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -27,8 +27,10 @@ parse_multisig, ) from ..psbt import PSBT -from ..serializations import ( +from ..tx import ( CTxOut, +) +from ..serializations import ( ser_uint256, ser_sig_der, ) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 12b906bce..a9dcb9e25 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -53,8 +53,10 @@ is_witness, ) from ..psbt import PSBT -from ..serializations import ( +from ..tx import ( CTransaction, +) +from ..serializations import ( ser_sig_der, ser_sig_compact, ser_string, diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 0b45b273c..8f69e9a9b 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -48,7 +48,7 @@ is_witness, ) from ..psbt import PSBT -from ..serializations import ( +from ..tx import ( CTransaction, ) import logging diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 00b923901..9bdddb722 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -56,8 +56,10 @@ is_witness, ) from ..psbt import PSBT -from ..serializations import ( +from ..tx import ( CTxOut, +) +from ..serializations import ( ser_uint256, ) from ..common import ( diff --git a/hwilib/psbt.py b/hwilib/psbt.py index 8a8227e38..628583364 100644 --- a/hwilib/psbt.py +++ b/hwilib/psbt.py @@ -13,10 +13,12 @@ from .key import KeyOriginInfo from .errors import PSBTSerializationError -from .serializations import ( +from .tx import ( CTransaction, CTxInWitness, CTxOut, +) +from .serializations import ( deser_string, Readable, ser_compact_size, diff --git a/hwilib/serializations.py b/hwilib/serializations.py index 992ccf032..c2f005162 100644 --- a/hwilib/serializations.py +++ b/hwilib/serializations.py @@ -9,32 +9,14 @@ Modified from the test/test_framework/mininode.py file from the Bitcoin repository -CTransaction,CTxIn, CTxOut, etc....: - data structures that should map to corresponding structures in - bitcoin/primitives for transactions only ser_*, deser_*: functions that handle serialization/deserialization """ -from .common import ( - hash256, -) -from ._script import ( - is_opreturn, - is_p2sh, - is_p2pkh, - is_p2pk, - is_witness, - is_p2wsh, -) - import struct -import copy from typing import ( List, - Optional, Sequence, - Tuple, TypeVar, Callable, ) @@ -193,255 +175,3 @@ def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: sig += r + s return sig - -# Objects that map to bitcoind objects, which can be serialized/deserialized - -MSG_WITNESS_FLAG = 1 << 30 - -class COutPoint(object): - def __init__(self, hash: int = 0, n: int = 0xffffffff): - self.hash = hash - self.n = n - - def deserialize(self, f: Readable) -> None: - self.hash = deser_uint256(f) - self.n = struct.unpack(" bytes: - r = b"" - r += ser_uint256(self.hash) - r += struct.pack(" str: - return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) - - -class CTxIn(object): - def __init__( - self, - outpoint: Optional[COutPoint] = None, - scriptSig: bytes = b"", - nSequence: int = 0, - ): - if outpoint is None: - self.prevout = COutPoint() - else: - self.prevout = outpoint - self.scriptSig = scriptSig - self.nSequence = nSequence - - def deserialize(self, f: Readable) -> None: - self.prevout = COutPoint() - self.prevout.deserialize(f) - self.scriptSig = deser_string(f) - self.nSequence = struct.unpack(" bytes: - r = b"" - r += self.prevout.serialize() - r += ser_string(self.scriptSig) - r += struct.pack(" str: - return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ - % (repr(self.prevout), self.scriptSig.hex(), - self.nSequence) - - -class CTxOut(object): - def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): - self.nValue = nValue - self.scriptPubKey = scriptPubKey - - def deserialize(self, f: Readable) -> None: - self.nValue = struct.unpack(" bytes: - r = b"" - r += struct.pack(" bool: - return is_opreturn(self.scriptPubKey) - - def is_p2sh(self) -> bool: - return is_p2sh(self.scriptPubKey) - - def is_p2wsh(self) -> bool: - return is_p2wsh(self.scriptPubKey) - - def is_p2pkh(self) -> bool: - return is_p2pkh(self.scriptPubKey) - - def is_p2pk(self) -> bool: - return is_p2pk(self.scriptPubKey) - - def is_witness(self) -> Tuple[bool, int, bytes]: - return is_witness(self.scriptPubKey) - - def __repr__(self) -> str: - return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ - % (self.nValue, self.nValue, self.scriptPubKey.hex()) - - -class CScriptWitness(object): - def __init__(self) -> None: - # stack is a vector of strings - self.stack: List[bytes] = [] - - def __repr__(self) -> str: - return "CScriptWitness(%s)" % \ - (",".join([x.hex() for x in self.stack])) - - def is_null(self) -> bool: - if self.stack: - return False - return True - - -class CTxInWitness(object): - def __init__(self) -> None: - self.scriptWitness = CScriptWitness() - - def deserialize(self, f: Readable) -> None: - self.scriptWitness.stack = deser_string_vector(f) - - def serialize(self) -> bytes: - return ser_string_vector(self.scriptWitness.stack) - - def __repr__(self) -> str: - return repr(self.scriptWitness) - - def is_null(self) -> bool: - return self.scriptWitness.is_null() - - -class CTxWitness(object): - def __init__(self) -> None: - self.vtxinwit: List[CTxInWitness] = [] - - def deserialize(self, f: Readable) -> None: - for i in range(len(self.vtxinwit)): - self.vtxinwit[i].deserialize(f) - - def serialize(self) -> bytes: - r = b"" - # This is different than the usual vector serialization -- - # we omit the length of the vector, which is required to be - # the same length as the transaction's vin vector. - for x in self.vtxinwit: - r += x.serialize() - return r - - def __repr__(self) -> str: - return "CTxWitness(%s)" % \ - (';'.join([repr(x) for x in self.vtxinwit])) - - def is_null(self) -> bool: - for x in self.vtxinwit: - if not x.is_null(): - return False - return True - - -class CTransaction(object): - def __init__(self, tx: Optional['CTransaction'] = None) -> None: - if tx is None: - self.nVersion = 1 - self.vin: List[CTxIn] = [] - self.vout: List[CTxOut] = [] - self.wit = CTxWitness() - self.nLockTime = 0 - self.sha256: Optional[int] = None - self.hash: Optional[str] = None - else: - self.nVersion = tx.nVersion - self.vin = copy.deepcopy(tx.vin) - self.vout = copy.deepcopy(tx.vout) - self.nLockTime = tx.nLockTime - self.sha256 = tx.sha256 - self.hash = tx.hash - self.wit = copy.deepcopy(tx.wit) - - def deserialize(self, f: Readable) -> None: - self.nVersion = struct.unpack(" bytes: - r = b"" - r += struct.pack(" bytes: - flags = 0 - if not self.wit.is_null(): - flags |= 1 - r = b"" - r += struct.pack(" bytes: - return self.serialize_without_witness() - - # Recalculate the txid (transaction hash without witness) - def rehash(self) -> None: - self.sha256 = None - self.calc_sha256() - - # We will only cache the serialization without witness in - # self.sha256 and self.hash -- those are expected to be the txid. - def calc_sha256(self, with_witness: bool = False) -> Optional[int]: - if with_witness: - # Don't cache the result, just return it - return uint256_from_str(hash256(self.serialize_with_witness())) - - if self.sha256 is None: - self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) - self.hash = hash256(self.serialize())[::-1].hex() - return None - - def is_null(self) -> bool: - return len(self.vin) == 0 and len(self.vout) == 0 - - def __repr__(self) -> str: - return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ - % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) diff --git a/hwilib/tx.py b/hwilib/tx.py new file mode 100644 index 000000000..0e7a519ed --- /dev/null +++ b/hwilib/tx.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Bitcoin Object Python Serializations + +Modified from the test/test_framework/mininode.py file from the +Bitcoin repository + +CTransaction,CTxIn, CTxOut, etc....: + data structures that should map to corresponding structures in + bitcoin/primitives for transactions only +""" + +import copy +import struct + +from .common import ( + hash256, +) +from ._script import ( + is_opreturn, + is_p2sh, + is_p2pkh, + is_p2pk, + is_witness, + is_p2wsh, +) +from .serializations import ( + deser_uint256, + deser_string, + deser_string_vector, + deser_vector, + Readable, + ser_uint256, + ser_string, + ser_string_vector, + ser_vector, + uint256_from_str, +) + +from typing import ( + List, + Optional, + Tuple, +) + +# Objects that map to bitcoind objects, which can be serialized/deserialized + +MSG_WITNESS_FLAG = 1 << 30 + +class COutPoint(object): + def __init__(self, hash: int = 0, n: int = 0xffffffff): + self.hash = hash + self.n = n + + def deserialize(self, f: Readable) -> None: + self.hash = deser_uint256(f) + self.n = struct.unpack(" bytes: + r = b"" + r += ser_uint256(self.hash) + r += struct.pack(" str: + return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) + + +class CTxIn(object): + def __init__( + self, + outpoint: Optional[COutPoint] = None, + scriptSig: bytes = b"", + nSequence: int = 0, + ): + if outpoint is None: + self.prevout = COutPoint() + else: + self.prevout = outpoint + self.scriptSig = scriptSig + self.nSequence = nSequence + + def deserialize(self, f: Readable) -> None: + self.prevout = COutPoint() + self.prevout.deserialize(f) + self.scriptSig = deser_string(f) + self.nSequence = struct.unpack(" bytes: + r = b"" + r += self.prevout.serialize() + r += ser_string(self.scriptSig) + r += struct.pack(" str: + return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ + % (repr(self.prevout), self.scriptSig.hex(), + self.nSequence) + + +class CTxOut(object): + def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): + self.nValue = nValue + self.scriptPubKey = scriptPubKey + + def deserialize(self, f: Readable) -> None: + self.nValue = struct.unpack(" bytes: + r = b"" + r += struct.pack(" bool: + return is_opreturn(self.scriptPubKey) + + def is_p2sh(self) -> bool: + return is_p2sh(self.scriptPubKey) + + def is_p2wsh(self) -> bool: + return is_p2wsh(self.scriptPubKey) + + def is_p2pkh(self) -> bool: + return is_p2pkh(self.scriptPubKey) + + def is_p2pk(self) -> bool: + return is_p2pk(self.scriptPubKey) + + def is_witness(self) -> Tuple[bool, int, bytes]: + return is_witness(self.scriptPubKey) + + def __repr__(self) -> str: + return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ + % (self.nValue, self.nValue, self.scriptPubKey.hex()) + + +class CScriptWitness(object): + def __init__(self) -> None: + # stack is a vector of strings + self.stack: List[bytes] = [] + + def __repr__(self) -> str: + return "CScriptWitness(%s)" % \ + (",".join([x.hex() for x in self.stack])) + + def is_null(self) -> bool: + if self.stack: + return False + return True + + +class CTxInWitness(object): + def __init__(self) -> None: + self.scriptWitness = CScriptWitness() + + def deserialize(self, f: Readable) -> None: + self.scriptWitness.stack = deser_string_vector(f) + + def serialize(self) -> bytes: + return ser_string_vector(self.scriptWitness.stack) + + def __repr__(self) -> str: + return repr(self.scriptWitness) + + def is_null(self) -> bool: + return self.scriptWitness.is_null() + + +class CTxWitness(object): + def __init__(self) -> None: + self.vtxinwit: List[CTxInWitness] = [] + + def deserialize(self, f: Readable) -> None: + for i in range(len(self.vtxinwit)): + self.vtxinwit[i].deserialize(f) + + def serialize(self) -> bytes: + r = b"" + # This is different than the usual vector serialization -- + # we omit the length of the vector, which is required to be + # the same length as the transaction's vin vector. + for x in self.vtxinwit: + r += x.serialize() + return r + + def __repr__(self) -> str: + return "CTxWitness(%s)" % \ + (';'.join([repr(x) for x in self.vtxinwit])) + + def is_null(self) -> bool: + for x in self.vtxinwit: + if not x.is_null(): + return False + return True + + +class CTransaction(object): + def __init__(self, tx: Optional['CTransaction'] = None) -> None: + if tx is None: + self.nVersion = 1 + self.vin: List[CTxIn] = [] + self.vout: List[CTxOut] = [] + self.wit = CTxWitness() + self.nLockTime = 0 + self.sha256: Optional[int] = None + self.hash: Optional[str] = None + else: + self.nVersion = tx.nVersion + self.vin = copy.deepcopy(tx.vin) + self.vout = copy.deepcopy(tx.vout) + self.nLockTime = tx.nLockTime + self.sha256 = tx.sha256 + self.hash = tx.hash + self.wit = copy.deepcopy(tx.wit) + + def deserialize(self, f: Readable) -> None: + self.nVersion = struct.unpack(" bytes: + r = b"" + r += struct.pack(" bytes: + flags = 0 + if not self.wit.is_null(): + flags |= 1 + r = b"" + r += struct.pack(" bytes: + return self.serialize_without_witness() + + # Recalculate the txid (transaction hash without witness) + def rehash(self) -> None: + self.sha256 = None + self.calc_sha256() + + # We will only cache the serialization without witness in + # self.sha256 and self.hash -- those are expected to be the txid. + def calc_sha256(self, with_witness: bool = False) -> Optional[int]: + if with_witness: + # Don't cache the result, just return it + return uint256_from_str(hash256(self.serialize_with_witness())) + + if self.sha256 is None: + self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) + self.hash = hash256(self.serialize())[::-1].hex() + return None + + def is_null(self) -> bool: + return len(self.vin) == 0 and len(self.vout) == 0 + + def __repr__(self) -> str: + return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ + % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) From 3e71c69da956ef93be761efdfcc45975945a2f6b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 18 Feb 2021 20:16:29 -0500 Subject: [PATCH 321/634] Make serializations private --- .cirrus.yml | 3 +-- hwilib/{serializations.py => _serialize.py} | 0 hwilib/devices/bitbox02.py | 2 +- hwilib/devices/digitalbitbox.py | 2 +- hwilib/devices/trezor.py | 2 +- hwilib/psbt.py | 2 +- hwilib/tx.py | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) rename hwilib/{serializations.py => _serialize.py} (100%) diff --git a/.cirrus.yml b/.cirrus.yml index 0514c50a7..7f01300e7 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -145,12 +145,11 @@ basic_test_task: hwilib/devices/trezor.py hwilib/errors.py hwilib/_script.py - hwilib/serializations.py + hwilib/_serialize.py hwilib/tx.py hwilib/hwwclient.py hwilib/__init__.py hwilib/key.py - hwilib/serializations.py hwilib/udevinstaller.py - name: Non-Device Tests test_script: cd test; poetry run ./run_tests.py; cd .. diff --git a/hwilib/serializations.py b/hwilib/_serialize.py similarity index 100% rename from hwilib/serializations.py rename to hwilib/_serialize.py diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 7f6e4e5a4..548d07fef 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -30,7 +30,7 @@ from ..tx import ( CTxOut, ) -from ..serializations import ( +from .._serialize import ( ser_uint256, ser_sig_der, ) diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index a9dcb9e25..4d678b641 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -56,7 +56,7 @@ from ..tx import ( CTransaction, ) -from ..serializations import ( +from .._serialize import ( ser_sig_der, ser_sig_compact, ser_string, diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 9bdddb722..e21d335de 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -59,7 +59,7 @@ from ..tx import ( CTxOut, ) -from ..serializations import ( +from .._serialize import ( ser_uint256, ) from ..common import ( diff --git a/hwilib/psbt.py b/hwilib/psbt.py index 628583364..24acbaada 100644 --- a/hwilib/psbt.py +++ b/hwilib/psbt.py @@ -18,7 +18,7 @@ CTxInWitness, CTxOut, ) -from .serializations import ( +from ._serialize import ( deser_string, Readable, ser_compact_size, diff --git a/hwilib/tx.py b/hwilib/tx.py index 0e7a519ed..d5f0c3835 100644 --- a/hwilib/tx.py +++ b/hwilib/tx.py @@ -28,7 +28,7 @@ is_witness, is_p2wsh, ) -from .serializations import ( +from ._serialize import ( deser_uint256, deser_string, deser_string_vector, From 8513a2494be2299412b46ab088f4fcad1935f349 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 15:29:41 -0400 Subject: [PATCH 322/634] Add Sphinx dependency and basic sphinx setup --- docs/conf.py | 55 +++++ docs/index.rst | 15 ++ poetry.lock | 539 +++++++++++++++++++++++++++++++++++++++++++------ pyproject.toml | 1 + 4 files changed, 544 insertions(+), 66 deletions(-) create mode 100644 docs/conf.py create mode 100644 docs/index.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..6a9653ae6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,55 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Hardware Wallet Interface' +copyright = '2021, The Hardware Wallet Interface Developers' +author = 'The Hardware Wallet Interface Developers' + +# The full version, including alpha/beta/rc tags +release = '2.0.0-dev' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..ee29433f6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,15 @@ +Welcome to Hardware Wallet Interface's documentation! +===================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/poetry.lock b/poetry.lock index f446c45b7..3562e5b3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "altgraph" version = "0.17" @@ -8,7 +16,7 @@ python-versions = "*" [[package]] name = "autopep8" -version = "1.5.4" +version = "1.5.5" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" category = "dev" optional = false @@ -18,6 +26,17 @@ python-versions = "*" pycodestyle = ">=2.6.0" toml = "*" +[[package]] +name = "babel" +version = "2.9.0" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + [[package]] name = "base58" version = "2.1.0" @@ -46,9 +65,17 @@ protobuf = ">=3.7" semver = ">=2.8.1" typing-extensions = ">=3.7.4" +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "cffi" -version = "1.14.4" +version = "1.14.5" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -57,24 +84,48 @@ python-versions = "*" [package.dependencies] pycparser = "*" +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "cryptography" -version = "3.3.1" +version = "3.4.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.12" -six = ">=1.4.1" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "ecdsa" @@ -121,6 +172,22 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "importlib-metadata" version = "3.4.0" @@ -137,9 +204,23 @@ zipp = ">=0.5" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + [[package]] name = "libusb1" -version = "1.9.1" +version = "1.9.2" description = "Pure-python wrapper for libusb-1.0" category = "main" optional = false @@ -156,6 +237,14 @@ python-versions = "*" [package.dependencies] altgraph = ">=0.15" +[[package]] +name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + [[package]] name = "mccabe" version = "0.6.1" @@ -207,6 +296,17 @@ python-versions = "~=3.5" [package.dependencies] cryptography = ">=2.8" +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + [[package]] name = "pefile" version = "2019.4.18" @@ -261,6 +361,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pygments" +version = "2.8.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "pyinstaller" version = "4.2" @@ -289,6 +397,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "pyside2" version = "5.15.2" @@ -300,6 +416,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10" [package.dependencies] shiboken2 = "5.15.2" +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pywin32-ctypes" version = "0.2.0" @@ -308,6 +432,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + [[package]] name = "semver" version = "2.13.0" @@ -332,6 +474,116 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "3.5.1" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.12" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "1.0.3" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.4" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + [[package]] name = "toml" version = "0.10.2" @@ -356,6 +608,19 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "zipp" version = "3.4.0" @@ -374,15 +639,24 @@ qt = ["pyside2"] [metadata] lock-version = "1.1" python-versions = "^3.6,<3.10" -content-hash = "569af9248ab36c24591d50a64650e507928acfcfea8258e08dde06a9659564ab" +content-hash = "ece80fa838f45c4e0f26b2def64ede901603792dcbeaaca889dfbce0c41aae21" [metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] altgraph = [ {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"}, {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, ] autopep8 = [ - {file = "autopep8-1.5.4.tar.gz", hash = "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"}, + {file = "autopep8-1.5.5-py2.py3-none-any.whl", hash = "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea"}, + {file = "autopep8-1.5.5.tar.gz", hash = "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"}, +] +babel = [ + {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"}, + {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] base58 = [ {file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"}, @@ -392,59 +666,69 @@ bitbox02 = [ {file = "bitbox02-5.2.0-py3-none-any.whl", hash = "sha256:1fffe76b1311ce43da34a8935dfbe497eb15edb0ca43729481265098d679f4f5"}, {file = "bitbox02-5.2.0.tar.gz", hash = "sha256:52b0b617660601939b30c8b588c28910946448b1b6d69ca231d5e3e47a322b71"}, ] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] cffi = [ - {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, - {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, - {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, - {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, - {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, - {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, - {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, - {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, - {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, - {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"}, - {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, - {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, - {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"}, - {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, - {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, - {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, - {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, - {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, - {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] cryptography = [ - {file = "cryptography-3.3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030"}, - {file = "cryptography-3.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0"}, - {file = "cryptography-3.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812"}, - {file = "cryptography-3.3.1-cp27-cp27m-win32.whl", hash = "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e"}, - {file = "cryptography-3.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901"}, - {file = "cryptography-3.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d"}, - {file = "cryptography-3.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5"}, - {file = "cryptography-3.3.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c"}, - {file = "cryptography-3.3.1-cp36-abi3-win32.whl", hash = "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a"}, - {file = "cryptography-3.3.1-cp36-abi3-win_amd64.whl", hash = "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7"}, - {file = "cryptography-3.3.1.tar.gz", hash = "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6"}, + {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"}, + {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"}, + {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"}, + {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] ecdsa = [ {file = "ecdsa-0.16.1-py2.py3-none-any.whl", hash = "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747"}, @@ -480,23 +764,89 @@ hidapi = [ {file = "hidapi-0.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:df4a23cd03f00d5cdc603252650df82cdd1923ceef6811cb029cc9d11a9a7a61"}, {file = "hidapi-0.10.1.tar.gz", hash = "sha256:a1170b18050bc57fae3840a51084e8252fd319c0fc6043d68c8501deb0e25846"}, ] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] importlib-metadata = [ {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, ] +jinja2 = [ + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, +] libusb1 = [ - {file = "libusb1-1.9.1-py2-none-any.whl", hash = "sha256:4a024fffe195c49f3e7eadd2266087b4be065982f0cb41ef4b7e2c5053e7e65c"}, - {file = "libusb1-1.9.1-py2-none-win32.whl", hash = "sha256:16203d77a1f623b6f8f4e6c9d6bac79c1293b8d3e11de5f2f3c30d91380ae478"}, - {file = "libusb1-1.9.1-py2-none-win_amd64.whl", hash = "sha256:3905e907156f0a3fade75ddf82a777a6a901b245aa14500429275d221a1606c2"}, - {file = "libusb1-1.9.1-py3-none-any.whl", hash = "sha256:46708965226154681f8e0b14c48325c6d02e253c218e5d3aeff846ec274ceda8"}, - {file = "libusb1-1.9.1-py3-none-win32.whl", hash = "sha256:3a53d94add2799eaa1b412e7a5e384486c9109745217b9ac7f94101ad0f41b96"}, - {file = "libusb1-1.9.1-py3-none-win_amd64.whl", hash = "sha256:b12666e8ad4df78e8f1bae36298c7d6f8f45d70ceea058b88631ef8478fd1eb0"}, - {file = "libusb1-1.9.1.tar.gz", hash = "sha256:d03ef15248c8b8ce440f6be4248eaadc074fc2dc5edd36c48e6e78eef3999292"}, + {file = "libusb1-1.9.2-py2-none-any.whl", hash = "sha256:2dff68819350bf8a8c157c7fa40d3efc741cb57868687d1714c8125ee99e8ac8"}, + {file = "libusb1-1.9.2-py2-none-win32.whl", hash = "sha256:c19d49136ef262474dbbac8bd40a2c4b65660220571de8564efec631c56bdc09"}, + {file = "libusb1-1.9.2-py2-none-win_amd64.whl", hash = "sha256:b4f25a2d66f62ec740edba3597038a7e9cd45b43456acfdb7a2bca8c2ad4aa30"}, + {file = "libusb1-1.9.2-py3-none-any.whl", hash = "sha256:c3dd4df43b5c38f65bf599413810d021f5f98396c4b6f66765fb98193aca11b0"}, + {file = "libusb1-1.9.2-py3-none-win32.whl", hash = "sha256:8ee4a963d4ecc20d9f4543b9151729c9cc9a229c2f9119e12bff762e84d8859f"}, + {file = "libusb1-1.9.2-py3-none-win_amd64.whl", hash = "sha256:a323588902fbd3693f8fddd7eac016700b24116c31b00756b9f52cf06c2a6629"}, + {file = "libusb1-1.9.2.tar.gz", hash = "sha256:27aec6aa1ff9ca845d0035023f3cf39710afac56903c51cd96a95404d064189e"}, ] macholib = [ {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"}, ] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -527,6 +877,11 @@ mypy-extensions = [ ] noiseprotocol = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, + {file = "noiseprotocol-0.3.1.tar.gz", hash = "sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645"}, +] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pefile = [ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, @@ -566,6 +921,10 @@ pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] +pygments = [ + {file = "Pygments-2.8.0-py3-none-any.whl", hash = "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"}, + {file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"}, +] pyinstaller = [ {file = "pyinstaller-4.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"}, ] @@ -573,6 +932,10 @@ pyinstaller-hooks-contrib = [ {file = "pyinstaller-hooks-contrib-2020.11.tar.gz", hash = "sha256:fc3290a2ca337d1d58c579c223201360bfe74caed6454eaf5a2550b77dbda45c"}, {file = "pyinstaller_hooks_contrib-2020.11-py2.py3-none-any.whl", hash = "sha256:fa8280b79d8a2b267a2e43ff44f73b3e4a68fc8d205b8d34e8e06c960f7c2fcf"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] pyside2 = [ {file = "PySide2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e"}, {file = "PySide2-5.15.2-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0558ced3bcd7f9da638fa8b7709dba5dae82a38728e481aac8b9058ea22fcdd9"}, @@ -581,10 +944,18 @@ pyside2 = [ {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75"}, {file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"}, ] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, @@ -601,6 +972,38 @@ six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] +snowballstemmer = [ + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, +] +sphinx = [ + {file = "Sphinx-3.5.1-py3-none-any.whl", hash = "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c"}, + {file = "Sphinx-3.5.1.tar.gz", hash = "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -642,6 +1045,10 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] +urllib3 = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] zipp = [ {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, diff --git a/pyproject.toml b/pyproject.toml index 1ddebddf0..e417543aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ macholib = {version = "^1.11",platform = "darwin"} autopep8 = "^1.4" flake8 = "^3.7" mypy = "^0.790" +sphinx = "^3.2.1" [tool.poetry.scripts] hwi = 'hwilib._cli:main' From 72316ee5eecc938031179cf998ae03d99e3c6232 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 15:53:23 -0400 Subject: [PATCH 323/634] Change bitcoin-core-usage.md to rst --- docs/bitcoin-core-usage.md | 294 ------------------------------------ docs/bitcoin-core-usage.rst | 294 ++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 294 deletions(-) delete mode 100644 docs/bitcoin-core-usage.md create mode 100644 docs/bitcoin-core-usage.rst diff --git a/docs/bitcoin-core-usage.md b/docs/bitcoin-core-usage.md deleted file mode 100644 index 4c92d6bb2..000000000 --- a/docs/bitcoin-core-usage.md +++ /dev/null @@ -1,294 +0,0 @@ -# Using Bitcoin Core with Hardware Wallets - -This approach is fairly manual, requires the command line, and Bitcoin Core >=0.18.0. - -Note: For this guide, code lines prefixed with `$` means that the command is typed in the terminal. Lines without `$` are output of the commands. - -### Disclaimer - -We are not liable for any coins that may be lost through this method. The software mentioned may have bugs. Use at your own risk. - -## Software - -### Bitcoin Core - -This method of using hardware wallets uses Bitcoin Core as the wallet for monitoring the blockchain. It allows a user to use their own full node instead of relying on an SPV wallet or vendor provided software. - -HWI works with Bitcoin Core as of commit [c576979b78b541bf3b4a7cbeee989b55d268e3e1](https://github.com/bitcoin/bitcoin/commit/c576979b78b541bf3b4a7cbeee989b55d268e3e1). It is usable with Bitcoin Core >=0.18.0. - -## Setup - -Clone Bitcoin Core and build it. Clone HWI. - -``` -$ git clone https://github.com/bitcoin/bitcoin.git -$ cd bitcoin -$ ./autogen.sh -$ ./configure -$ make -$ src/bitcoind -daemon -addresstype=bech32 -changetype=bech32 -$ cd .. -$ git clone https://github.com/bitcoin-core/HWI.git -$ cd HWI -$ python3 setup.py install -``` - -You may need some dependencies, on ubuntu install `libudev-dev` and `libusb-1.0-0-dev` - -Now we need to find our hardware wallet. We do this using: - -``` -$ ./hwi.py enumerate -[{"fingerprint": "8038ecd9", "serial_number": "205A32753042", "type": "coldcard", "path": "0001:0005:00"}] -``` - -For this example, we will use the Coldcard. As we can see, the device path is `0001:0005:00`. The fingerprint of the master key is `8038ecd9`. Now that we have the device, we can issue commands to it. So now we want to get some keys and import them into Core. -We will be fetching keys at the BIP 84 default. If `--path` and `--internal` are not -specified, both receiving and change address descriptors are generated. - -``` -$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 0 1000 -[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}] -``` - -We now create a new Bitcoin Core wallet and import the keys into Bitcoin Core. The output is formatted properly for Bitcoin Core so it can be copy and pasted. - -``` -$ ../bitcoin/src/bitcoin-cli createwallet "coldcard" true -{ - "name": "coldcard", - "warning": "" -} -$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}]' - -[ - { - "success": true - }, - { - "success": true - } -] -``` - -The Bitcoin Core wallet is now setup to watch two thousand keys (1000 normal, 1000 change) from your hardware wallet and you can use it to track your balances and create transactions. The transactions will need to be signed through HWI. - -If the wallet was previously used, you will need to rescan the blockchain. You can either do this using the `rescanblockchain` command or editing the `timestamp` in the `importmulti` command. -Here are some examples (`` refers to a block height before the wallet was created). - -``` -$ ../bitcoin/src/bitcoin-cli rescanblockchain -$ ../bitcoin/src/bitcoin-cli rescanblockchain 500000 # Rescan from block 500000 - -$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": , "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' -$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": 500000, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' # Imports and rescans from block 500000 -``` - -## Usage - -Usage of this primarily involves Bitcoin Core. Currently the GUI only supports generating new receive addresses (once all of the keys are imported) so this guide will only cover the command line. - -### Receiving - -From the folder containing `bitcoin` and `HWI`, go into `bitcoin`. We will be doing most of the commands here. - -``` -$ cd bitcoin -``` - -To get a new address, use `getnewaddress` as you normally would - -``` -$ src/bitcoin-cli -rpcwallet=coldcard getnewaddress -bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s -``` - -This address belongs to your hardware wallet. You can check this by doing `getaddressinfo`: - -``` -$ src/bitcoin-cli -rpcwallet=coldcard getaddressinfo bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s -{ - "address": "bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s", - "scriptPubKey": "0014e1c1955440a655dbdeb3b7f48a1206f86719912f", - "ismine": false, - "iswatchonly": true, - "solvable": true, - "isscript": false, - "iswitness": true, - "witness_version": 0, - "witness_program": "e1c1955440a655dbdeb3b7f48a1206f86719912f", - "pubkey": "022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d4", - "label": "", - "ischange": false, - "timestamp": 1541688305, - "hdkeypath": "m/84'/1'/0'/0/0", - "hdseedid": "0000000000000000000000000000000000000000", - "hdmasterkeyid": "00000000000000000000000000000000d9ec3880", - "labels": [ - { - "name": "", - "purpose": "receive" - } - ] -} - -``` -Notice how the pubkey is the one that was specified as the very first thing being imported to your wallet. - -You can give this out to people as you normally would. When coins are sent to it, you will see them in your Bitcoin Core wallet as watch-only. - -## Sending - -To send Bitcoin, we will use `walletcreatefundedpsbt`. This will create a Partially Signed Bitcoin Transaction which is funded by inputs from the wallets (i.e. your watching only inputs selected with Bitcoin Core's coin selection algorithm). -This PSBT can be used with HWI to produce a signed PSBT which can then be finalized and broadcast. - -For example, suppose I am sending to 1 BTC to bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy. First I create a funded psbt with BIP 32 derivation paths to be included: -``` -$ src/bitcoin-cli -rpcwallet=coldcard walletcreatefundedpsbt '[]' '[{"bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy":1}]' 0 '{"includeWatching":true}' true -{ - "psbt": "cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA", - "fee": 0.00002820, - "changepos": 1 -} - -``` - -Now I take the updated psbt and inspect it with `decodepsbt`: - -``` -$ src/bitcoin-cli decodepsbt cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA -{ - "tx": { - "txid": "e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf", - "hash": "e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf", - "version": 2, - "size": 113, - "vsize": 113, - "weight": 452, - "locktime": 0, - "vin": [ - { - "txid": "b61f6f2e9a11558bcbdf12dfcb5dbd5aa1cbde621e9918600c7eec94405a0a4f", - "vout": 0, - "scriptSig": { - "asm": "", - "hex": "" - }, - "sequence": 4294967294 - } - ], - "vout": [ - { - "value": 1.00000000, - "n": 0, - "scriptPubKey": { - "asm": "0 553c2a2fdabe5b88e9dbd899d7d0e57cc91b19be", - "hex": "0014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19be", - "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": [ - "bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy" - ] - } - }, - { - "value": 3.99997180, - "n": 1, - "scriptPubKey": { - "asm": "0 b1ee5f7591b8fb37ca97903b388dc39a859411fc", - "hex": "0014b1ee5f7591b8fb37ca97903b388dc39a859411fc", - "reqSigs": 1, - "type": "witness_v0_keyhash", - "addresses": [ - "bc1qk8h97av3hran0j5hjqan3rwrn2zegy0unusy49" - ] - } - } - ] - }, - "unknown": { - }, - "inputs": [ - { - "witness_utxo": { - "amount": 5.00000000, - "scriptPubKey": { - "asm": "0 e1c1955440a655dbdeb3b7f48a1206f86719912f", - "hex": "0014e1c1955440a655dbdeb3b7f48a1206f86719912f", - "type": "witness_v0_keyhash", - "address": "bc1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0wyd5k2" - } - }, - "bip32_derivs": [ - { - "pubkey": "022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d4", - "master_fingerprint": "8038ecd9", - "path": "m/84'/1'/0'/0/0" - } - ] - } - ], - "outputs": [ - { - }, - { - "bip32_derivs": [ - { - "pubkey": "03f41cc4362baf77cc25d30ae7415337a60e1c4b9851844ce9c057bbe00f3dabf5", - "master_fingerprint": "8038ecd9", - "path": "m/84'/1'/0'/1/0" - } - ] - } - ], - "fee": 0.00002820 -} - -``` - -Once the transaction has been inspected and everything looks good, the transaction can now be signed using HWI. - -``` -$ cd ../HWI -$ ./hwi.py -f 8038ecd9 --chain test signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA - -``` -Follow the onscreen instructions, check everything, and approve the transaction. The result will look like: -``` -{"psbt": "cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgICIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9RIMEUCIQDMECVXsrFK5XbMQn5yVCvm3zWF1kdCgepf3RSqFDDmAAIgQtty07rN4zBWMjd1qVOtkgOHBAlGaO2Se3LkiNsABYcBAQMEAQAAACIGAiMg8c9y57os72vjLXSTzjvUxqJXX+Uc4mA3etwWVgPUGIA47NlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgID9BzENiuvd8wl0wrnQVM3pg4cS5hRhEzpwFe74A89q/UYgDjs2VQAAIABAACAAAAAgAEAAAAAAAAAAA=="} -``` - -We can now take the PSBT, finalize it, and broadcast it with Bitcoin Core - -``` -$ cd ../bitcoin -$ src/bitcoin-cli finalizepsbt cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgICIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9RIMEUCIQDMECVXsrFK5XbMQn5yVCvm3zWF1kdCgepf3RSqFDDmAAIgQtty07rN4zBWMjd1qVOtkgOHBAlGaO2Se3LkiNsABYcBAQMEAQAAACIGAiMg8c9y57os72vjLXSTzjvUxqJXX+Uc4mA3etwWVgPUGIA47NlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgID9BzENiuvd8wl0wrnQVM3pg4cS5hRhEzpwFe74A89q/UYgDjs2VQAAIABAACAAAAAgAEAAAAAAAAAAA== -{ - "hex": "020000000001014f0a5a4094ec7e0c6018991e62decba15abd5dcbdf12dfcb8b55119a2e6f1fb60000000000feffffff0200e1f50500000000160014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19befc78d71700000000160014b1ee5f7591b8fb37ca97903b388dc39a859411fc02483045022100cc102557b2b14ae576cc427e72542be6df3585d6474281ea5fdd14aa1430e600022042db72d3bacde33056323775a953ad92038704094668ed927b72e488db0005870121022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d400000000", - "complete": true -} -$ src/bitcoin-cli sendrawtransaction 020000000001014f0a5a4094ec7e0c6018991e62decba15abd5dcbdf12dfcb8b55119a2e6f1fb60000000000feffffff0200e1f50500000000160014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19befc78d71700000000160014b1ee5f7591b8fb37ca97903b388dc39a859411fc02483045022100cc102557b2b14ae576cc427e72542be6df3585d6474281ea5fdd14aa1430e600022042db72d3bacde33056323775a953ad92038704094668ed927b72e488db0005870121022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d400000000 -e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf -``` - -### Refilling the keypools - -When the keypools run out, they can be refilled by using the `getkeypool` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following `getkeypool` commands: - -``` -$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 1000 2000 -$ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh --internal 1000 2000 -``` -The output can be imported with `importmulti` as shown in the Setup steps. - -## Derivation Path BIP Compliance - -The instructions above use BIP 84 to derive keys used for P2WPKH addresses (bech32 addresses). -HWI follows BIPs 44, 84, and 49. By default, descriptors will be for P2PKH addresses with keys derived at `m/44h/0h/0h/0` for normal receiving keys and `m/44h/0h/0h/1` for change keys. -Using the `--addr-type wpkh` option will result in P2WPKH addresses with keys derived at `m/84h/0h/0h/0` for normal receiving keys and `m/84h/0h/0h/1` for change keys. -Using the `--addr-type sh_wpkh` option will result in P2SH nested P2WPKH addresses with keys derived at `m/49h/0h/0h/0` for normal receiving keys and `m/49h/0h/0h/1` for change keys. - -To actually get the correct address type when using `getnewaddress` from Bitcoin Core, you will need to additionally set `-addresstype=p2sh-segwit` and `-changetype=p2sh-segwit`. -This can be set in the command line (as shown in the example) or in your bitcoin.conf file. - -Alternative derivation paths can also be chosen using the `--path` option and specifying your own derivation path. diff --git a/docs/bitcoin-core-usage.rst b/docs/bitcoin-core-usage.rst new file mode 100644 index 000000000..87d2fa002 --- /dev/null +++ b/docs/bitcoin-core-usage.rst @@ -0,0 +1,294 @@ +Using Bitcoin Core with Hardware Wallets +**************************************** + +This approach is fairly manual, requires the command line, and Bitcoin Core >=0.18.0. + +Note: For this guide, code lines prefixed with ``$`` means that the command is typed in the terminal. Lines without ``$`` are output of the commands. + +Disclaimer +========== + +We are not liable for any coins that may be lost through this method. The software mentioned may have bugs. Use at your own risk. + +Software +-------- + +Bitcoin Core +^^^^^^^^^^^^ + +This method of using hardware wallets uses Bitcoin Core as the wallet for monitoring the blockchain. It allows a user to use their own full node instead of relying on an SPV wallet or vendor provided software. + +HWI works with Bitcoin Core as of commit `c576979b78b541bf3b4a7cbeee989b55d268e3e1 `_ It is usable with Bitcoin Core >=0.18.0. + +Setup +===== + +Clone Bitcoin Core and build it. Clone HWI. + +:: + + $ git clone https://github.com/bitcoin/bitcoin.git + $ cd bitcoin + $ ./autogen.sh + $ ./configure + $ make + $ src/bitcoind -daemon -addresstype=bech32 -changetype=bech32 + $ cd .. + $ git clone https://github.com/bitcoin-core/HWI.git + $ cd HWI + $ python3 setup.py install + +You may need some dependencies, on ubuntu install ``libudev-dev`` and ``libusb-1.0-0-dev`` + +Now we need to find our hardware wallet. We do this using:: + + $ ./hwi.py enumerate + [{"fingerprint": "8038ecd9", "serial_number": "205A32753042", "type": "coldcard", "path": "0001:0005:00"}] + +For this example, we will use the Coldcard. As we can see, the device path is ``0001:0005:00``. The fingerprint of the master key is ``8038ecd9``. Now that we have the device, we can issue commands to it. So now we want to get some keys and import them into Core. +We will be fetching keys at the BIP 84 default. If ``--path`` and ``--internal`` are not +specified, both receiving and change address descriptors are generated. + +:: + + $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 0 1000 + [{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}] + +We now create a new Bitcoin Core wallet and import the keys into Bitcoin Core. The output is formatted properly for Bitcoin Core so it can be copy and pasted. + +:: + + $ ../bitcoin/src/bitcoin-cli createwallet "coldcard" true + { + "name": "coldcard", + "warning": "" + } + $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}]' + + [ + { + "success": true + }, + { + "success": true + } + ] + +The Bitcoin Core wallet is now setup to watch two thousand keys (1000 normal, 1000 change) from your hardware wallet and you can use it to track your balances and create transactions. The transactions will need to be signed through HWI. + +If the wallet was previously used, you will need to rescan the blockchain. You can either do this using the ``rescanblockchain`` command or editing the ``timestamp`` in the ``importmulti`` command. +Here are some examples (```` refers to a block height before the wallet was created). + +:: + + $ ../bitcoin/src/bitcoin-cli rescanblockchain + $ ../bitcoin/src/bitcoin-cli rescanblockchain 500000 # Rescan from block 500000 + + $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": , "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' + $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": 500000, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' # Imports and rescans from block 500000 + +Usage +===== + +Usage of this primarily involves Bitcoin Core. Currently the GUI only supports generating new receive addresses (once all of the keys are imported) so this guide will only cover the command line. + +Receiving +--------- + +From the folder containing ``bitcoin`` and ``HWI``, go into ``bitcoin``. We will be doing most of the commands here. + +:: + + $ cd bitcoin + +To get a new address, use ``getnewaddress`` as you normally would + +:: + + $ src/bitcoin-cli -rpcwallet=coldcard getnewaddress + bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s + +This address belongs to your hardware wallet. You can check this by doing ``getaddressinfo``:: + + $ src/bitcoin-cli -rpcwallet=coldcard getaddressinfo bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s + { + "address": "bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s", + "scriptPubKey": "0014e1c1955440a655dbdeb3b7f48a1206f86719912f", + "ismine": false, + "iswatchonly": true, + "solvable": true, + "isscript": false, + "iswitness": true, + "witness_version": 0, + "witness_program": "e1c1955440a655dbdeb3b7f48a1206f86719912f", + "pubkey": "022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d4", + "label": "", + "ischange": false, + "timestamp": 1541688305, + "hdkeypath": "m/84'/1'/0'/0/0", + "hdseedid": "0000000000000000000000000000000000000000", + "hdmasterkeyid": "00000000000000000000000000000000d9ec3880", + "labels": [ + { + "name": "", + "purpose": "receive" + } + ] + } + +Notice how the pubkey is the one that was specified as the very first thing being imported to your wallet. + +You can give this out to people as you normally would. When coins are sent to it, you will see them in your Bitcoin Core wallet as watch-only. + +Sending +======= + +To send Bitcoin, we will use ``walletcreatefundedpsbt``. This will create a Partially Signed Bitcoin Transaction which is funded by inputs from the wallets (i.e. your watching only inputs selected with Bitcoin Core's coin selection algorithm). +This PSBT can be used with HWI to produce a signed PSBT which can then be finalized and broadcast. + +For example, suppose I am sending to 1 BTC to bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy. First I create a funded psbt with BIP 32 derivation paths to be included:: + + $ src/bitcoin-cli -rpcwallet=coldcard walletcreatefundedpsbt '[]' '[{"bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy":1}]' 0 '{"includeWatching":true}' true + { + "psbt": "cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA", + "fee": 0.00002820, + "changepos": 1 + } + + +Now I take the updated psbt and inspect it with ``decodepsbt``:: + + $ src/bitcoin-cli decodepsbt cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA + { + "tx": { + "txid": "e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf", + "hash": "e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf", + "version": 2, + "size": 113, + "vsize": 113, + "weight": 452, + "locktime": 0, + "vin": [ + { + "txid": "b61f6f2e9a11558bcbdf12dfcb5dbd5aa1cbde621e9918600c7eec94405a0a4f", + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "sequence": 4294967294 + } + ], + "vout": [ + { + "value": 1.00000000, + "n": 0, + "scriptPubKey": { + "asm": "0 553c2a2fdabe5b88e9dbd899d7d0e57cc91b19be", + "hex": "0014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19be", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy" + ] + } + }, + { + "value": 3.99997180, + "n": 1, + "scriptPubKey": { + "asm": "0 b1ee5f7591b8fb37ca97903b388dc39a859411fc", + "hex": "0014b1ee5f7591b8fb37ca97903b388dc39a859411fc", + "reqSigs": 1, + "type": "witness_v0_keyhash", + "addresses": [ + "bc1qk8h97av3hran0j5hjqan3rwrn2zegy0unusy49" + ] + } + } + ] + }, + "unknown": { + }, + "inputs": [ + { + "witness_utxo": { + "amount": 5.00000000, + "scriptPubKey": { + "asm": "0 e1c1955440a655dbdeb3b7f48a1206f86719912f", + "hex": "0014e1c1955440a655dbdeb3b7f48a1206f86719912f", + "type": "witness_v0_keyhash", + "address": "bc1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0wyd5k2" + } + }, + "bip32_derivs": [ + { + "pubkey": "022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d4", + "master_fingerprint": "8038ecd9", + "path": "m/84'/1'/0'/0/0" + } + ] + } + ], + "outputs": [ + { + }, + { + "bip32_derivs": [ + { + "pubkey": "03f41cc4362baf77cc25d30ae7415337a60e1c4b9851844ce9c057bbe00f3dabf5", + "master_fingerprint": "8038ecd9", + "path": "m/84'/1'/0'/1/0" + } + ] + } + ], + "fee": 0.00002820 + } + +Once the transaction has been inspected and everything looks good, the transaction can now be signed using HWI. + +:: + + $ cd ../HWI + $ ./hwi.py -f 8038ecd9 --testnet signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA + +Follow the onscreen instructions, check everything, and approve the transaction. The result will look like:: + + {"psbt": "cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgICIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9RIMEUCIQDMECVXsrFK5XbMQn5yVCvm3zWF1kdCgepf3RSqFDDmAAIgQtty07rN4zBWMjd1qVOtkgOHBAlGaO2Se3LkiNsABYcBAQMEAQAAACIGAiMg8c9y57os72vjLXSTzjvUxqJXX+Uc4mA3etwWVgPUGIA47NlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgID9BzENiuvd8wl0wrnQVM3pg4cS5hRhEzpwFe74A89q/UYgDjs2VQAAIABAACAAAAAgAEAAAAAAAAAAA=="} + +We can now take the PSBT, finalize it, and broadcast it with Bitcoin Core + +:: + + $ cd ../bitcoin + $ src/bitcoin-cli finalizepsbt cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgICIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9RIMEUCIQDMECVXsrFK5XbMQn5yVCvm3zWF1kdCgepf3RSqFDDmAAIgQtty07rN4zBWMjd1qVOtkgOHBAlGaO2Se3LkiNsABYcBAQMEAQAAACIGAiMg8c9y57os72vjLXSTzjvUxqJXX+Uc4mA3etwWVgPUGIA47NlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgID9BzENiuvd8wl0wrnQVM3pg4cS5hRhEzpwFe74A89q/UYgDjs2VQAAIABAACAAAAAgAEAAAAAAAAAAA== + { + "hex": "020000000001014f0a5a4094ec7e0c6018991e62decba15abd5dcbdf12dfcb8b55119a2e6f1fb60000000000feffffff0200e1f50500000000160014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19befc78d71700000000160014b1ee5f7591b8fb37ca97903b388dc39a859411fc02483045022100cc102557b2b14ae576cc427e72542be6df3585d6474281ea5fdd14aa1430e600022042db72d3bacde33056323775a953ad92038704094668ed927b72e488db0005870121022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d400000000", + "complete": true + } + $ src/bitcoin-cli sendrawtransaction 020000000001014f0a5a4094ec7e0c6018991e62decba15abd5dcbdf12dfcb8b55119a2e6f1fb60000000000feffffff0200e1f50500000000160014553c2a2fdabe5b88e9dbd899d7d0e57cc91b19befc78d71700000000160014b1ee5f7591b8fb37ca97903b388dc39a859411fc02483045022100cc102557b2b14ae576cc427e72542be6df3585d6474281ea5fdd14aa1430e600022042db72d3bacde33056323775a953ad92038704094668ed927b72e488db0005870121022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d400000000 + e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf + +Refilling the keypools +---------------------- + +When the keypools run out, they can be refilled by using the ``getkeypool`` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following ``getkeypool`` commands:: + + $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 1000 2000 + $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh --internal 1000 2000 + +The output can be imported with ``importmulti`` as shown in the Setup steps. + +Derivation Path BIP Compliance +============================== + +The instructions above use BIP 84 to derive keys used for P2WPKH addresses (bech32 addresses). +HWI follows BIPs 44, 84, and 49. By default, descriptors will be for P2PKH addresses with keys derived at ``m/44h/0h/0h/0`` for normal receiving keys and ``m/44h/0h/0h/1`` for change keys. +Using the ``--addr-type wpkh`` option will result in P2WPKH addresses with keys derived at ``m/84h/0h/0h/0`` for normal receiving keys and ``m/84h/0h/0h/1`` for change keys. +Using the ``--addr-type sh_wpkh`` option will result in P2SH nested P2WPKH addresses with keys derived at ``m/49h/0h/0h/0`` for normal receiving keys and ``m/49h/0h/0h/1`` for change keys. + +To actually get the correct address type when using ``getnewaddress`` from Bitcoin Core, you will need to additionally set ``-addresstype=p2sh-segwit`` and ``-changetype=p2sh-segwit``. +This can be set in the command line (as shown in the example) or in your bitcoin.conf file. + +Alternative derivation paths can also be chosen using the ``--path`` option and specifying your own derivation path. From c340c35402a64a6d229f3385ab547e1e069a99aa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:05:26 -0500 Subject: [PATCH 324/634] Move Coldcard docs to docstring. --- docs/coldcard.md | 29 ---------------------- hwilib/devices/coldcard.py | 51 +++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 30 deletions(-) delete mode 100644 docs/coldcard.md diff --git a/docs/coldcard.md b/docs/coldcard.md deleted file mode 100644 index b5ca98c9c..000000000 --- a/docs/coldcard.md +++ /dev/null @@ -1,29 +0,0 @@ -# ColdCard - -The ColdCard is partially supported by HWI - -Current implemented commands are: - -* `getmasterxpub` -* `signtx` (only single key) -* `getxpub` -- `setup` -- `wipe` -- `restore` -- `backup` -- `displayaddress` -- `signmessage` - -## Notes on `setup`, `wipe`, and `restore` - -The Coldcard does not allow you to setup, wipe, or restore the device via software. That is done on the device itself. The implementation here is just to let users know those commands do not work. - -## Note on `backup` - -The `backup` command will create a backup file in the current working directory. This file is protected by the passphrase shown on the Coldcard during the backup process. - -## Caveat for `signtx` - -- The Coldcard firmware only supports signing single key and multisig transactions. It cannot sign arbitrary scripts. -- Multisigs need to be registered on the device before a transaction spending that multisig will be signed by the device. -- Multisigs must use BIP 67. This can be accomplished in Bitcoin Core using the `sortedmulti()` descriptor, available in Bitcoin Core 0.20. diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 18e0cd120..ca14bd05c 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -1,4 +1,7 @@ -# Coldcard interaction script +""" +Coldcard +******** +""" from typing import ( Dict, @@ -50,6 +53,7 @@ AddressType, Chain, ) +from functools import wraps from hashlib import sha256 import base64 @@ -70,6 +74,7 @@ def coldcard_exception(f: Callable[..., Any]) -> Callable[..., Any]: + @wraps(f) def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) @@ -111,6 +116,13 @@ def get_master_fingerprint(self) -> bytes: @coldcard_exception def sign_tx(self, tx: PSBT) -> PSBT: + """ + Sign a transaction with the Coldcard. + + - The Coldcard firmware only supports signing single key and multisig transactions. It cannot sign arbitrary scripts. + - Multisigs need to be registered on the device before a transaction spending that multisig will be signed by the device. + - Multisigs must use BIP 67. This can be accomplished in Bitcoin Core using the `sortedmulti()` descriptor, available in Bitcoin Core 0.20. + """ self.device.check_mitm() # Get this devices master key fingerprint @@ -282,16 +294,38 @@ def display_multisig_address( return address def setup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + The Coldcard does not support setup via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not support software setup') def wipe_device(self) -> bool: + """ + The Coldcard does not support wiping via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not support wiping via software') def restore_device(self, label: str = "", word_count: int = 24) -> bool: + """ + The Coldcard does not support restoring via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not support restoring via software') @coldcard_exception def backup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + Creates a backup file in the current working directory. This file is protected by the + passphrase shown on the Coldcard. + + :param label: Value is ignored + :param passphrase: Value is ignored + """ self.device.check_mitm() ok = self.device.send_recv(CCProtocolPacker.start_backup()) @@ -323,12 +357,27 @@ def close(self) -> None: self.device.close() def prompt_pin(self) -> bool: + """ + The Coldcard does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') def send_pin(self, pin: str) -> bool: + """ + The Coldcard does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not need a PIN sent from the host') def toggle_passphrase(self) -> bool: + """ + The Coldcard does not support toggling passphrase from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host') def enumerate(password: str = "") -> List[Dict[str, Any]]: From 073c9354b40f1e570e70c1ae5cf9fac343460df2 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:17:22 -0500 Subject: [PATCH 325/634] Move bitbox01 docs to docstring --- docs/digitalbitbox.md | 35 ----------------------- hwilib/devices/digitalbitbox.py | 50 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 36 deletions(-) delete mode 100644 docs/digitalbitbox.md diff --git a/docs/digitalbitbox.md b/docs/digitalbitbox.md deleted file mode 100644 index 1c83efeba..000000000 --- a/docs/digitalbitbox.md +++ /dev/null @@ -1,35 +0,0 @@ -# Digital BitBox - -The Digital BitBox is supported by HWI - -Current implemented commands are: - -* `getmasterxpub` -* `signtx` -* `getxpub` (with some caveats) -- `setup` -- `wipe` -- `restore` -- `backup` -- `displayaddress` -- `signmessage` - -## Usage Notes - -You must specify your Digital BitBox password using the `-p` option. E.g. - -``` -./hwi.py -t digitalbitbox -d 0001:0001:00 -p password getmasterxpub -``` - -## `getxpub` Caveats - -The Digital BitBox requires that one of the levels in the derivation path is hardened. - -## Note on `restore` - -The Digital BitBox does not allow users to restore a backup or seed via software. - -## Note on `displayaddress` - -The Digital BitBox does not have a screen to display an address on, so the implementation just raises an error stating this. diff --git a/hwilib/devices/digitalbitbox.py b/hwilib/devices/digitalbitbox.py index 4d678b641..987d0f003 100644 --- a/hwilib/devices/digitalbitbox.py +++ b/hwilib/devices/digitalbitbox.py @@ -1,4 +1,7 @@ -# Digital Bitbox interaction script +""" +BitBox01 +******** +""" import hid import struct @@ -13,6 +16,7 @@ import socket import sys import time +from functools import wraps from typing import ( Any, Callable, @@ -152,6 +156,7 @@ def __str__(self) -> str: return 'Error: {}, Code: {}'.format(self.error['error']['message'], self.error['error']['code']) def digitalbitbox_exception(f: Callable[..., Any]) -> Any: + @wraps(f) def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) @@ -344,6 +349,13 @@ def format_backup_filename(name: str) -> str: class DigitalbitboxClient(HardwareWalletClient): def __init__(self, path: str, password: str, expert: bool = False) -> None: + """ + The `DigitalbitboxClient` is a `HardwareWalletClient` for interacting with BitBox01 devices (previously known as the Digital BitBox). + + :param path: Path to the device as given by `enumerate` + :param password: The password required to communicate with the device. Must be provided. + :param expert: Whether to be in expert mode and return additional information. + """ super(DigitalbitboxClient, self).__init__(path, password, expert) if not password: raise NoPasswordError('Password must be supplied for digital BitBox') @@ -359,6 +371,12 @@ def __init__(self, path: str, password: str, expert: bool = False) -> None: @digitalbitbox_exception def get_pubkey_at_path(self, path: str) -> ExtendedKey: + """ + Retrieve the public key at the path. + The BitBox01 requires that at least one of the levels in the path is hardened. + + :param path: Path to retrieve the public key at. + """ if '\'' not in path and 'h' not in path and 'H' not in path: raise BadArgumentError('The digital bitbox requires one part of the derivation path to be derived using hardened keys') reply = send_encrypt('{"xpub":"' + path + '"}', self.password, self.device) @@ -556,9 +574,19 @@ def sign_message(self, message: Union[str, bytes], keypath: str) -> str: return base64.b64encode(compact_sig).decode('utf-8') def display_singlesig_address(self, keypath: str, addr_type: AddressType) -> str: + """ + The BitBox01 does not have a screen to display addresses on. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') def display_multisig_address(self, threshold: int, pubkeys: List[PubkeyProvider], addr_type: AddressType) -> str: + """ + The BitBox01 does not have a screen to display addresses on. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on') @digitalbitbox_exception @@ -593,6 +621,11 @@ def wipe_device(self) -> bool: return True def restore_device(self, label: str = "", word_count: int = 24) -> bool: + """ + The BitBox01 does not support restoring via software. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not support restoring via software') @digitalbitbox_exception @@ -613,12 +646,27 @@ def close(self) -> None: self.device.close() def prompt_pin(self) -> bool: + """ + The BitBox01 does not need a PIN sent from the host. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') def send_pin(self, pin: str) -> bool: + """ + The BitBox01 does not need a PIN sent from the host. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host') def toggle_passphrase(self) -> bool: + """ + The BitBox01 does not support toggling passphrase from the host. + + :raises UnaavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host') def enumerate(password: str = "") -> List[Dict[str, Any]]: From a71768663ffde6a66c668a394bf4c48d9df86cbc Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:19:42 -0500 Subject: [PATCH 326/634] Move keepkey docs to docstring --- docs/keepkey.md | 27 --------------------------- hwilib/devices/keepkey.py | 10 +++++++++- 2 files changed, 9 insertions(+), 28 deletions(-) delete mode 100644 docs/keepkey.md diff --git a/docs/keepkey.md b/docs/keepkey.md deleted file mode 100644 index 7b1d12f42..000000000 --- a/docs/keepkey.md +++ /dev/null @@ -1,27 +0,0 @@ -# KeepKey - -The KeepKey is partially supported by HWI - -Current implemented commands are: - -* `getmasterxpub` -* `getxpub` -- `setup` -- `wipe` -- `restore` -- `backup` -- `signtx` -- `displayaddress` -- `signmessage` -- `togglepassphrase` - -## `signtx` Caveats - -Due to the limitations of the KeepKey, some transactions cannot be signed by a KeepKey. - -- Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. -* Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. - -## Note on `backup` - -Once the device is backed up at setup, the seed words will not be shown again to be backed up. The implementation here lets users know that `backup` does not work. diff --git a/hwilib/devices/keepkey.py b/hwilib/devices/keepkey.py index ceb2645c7..10d45f171 100644 --- a/hwilib/devices/keepkey.py +++ b/hwilib/devices/keepkey.py @@ -1,4 +1,7 @@ -# KeepKey interaction script +""" +Keepkey +******* +""" from ..errors import ( DEVICE_NOT_INITIALIZED, @@ -137,6 +140,11 @@ def get_fields(cls) -> Dict[int, p.FieldType]: class KeepkeyClient(TrezorClient): def __init__(self, path: str, password: str = "", expert: bool = False) -> None: + """ + The `KeepkeyClient` is a `HardwareWalletClient` for interacting with the Keepkey. + + As Keepkeys are clones of the Trezor 1, please refer to `TrezorClient` for documentation. + """ super(KeepkeyClient, self).__init__(path, password, expert) self.type = 'Keepkey' self.client.vendors = ("keepkey.com") From 2b99ca230191b04a1b362b033615617c91bb7c30 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:23:14 -0500 Subject: [PATCH 327/634] Move trezor docs to docstring --- docs/trezor.md | 27 --------------------------- hwilib/devices/trezor.py | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 docs/trezor.md diff --git a/docs/trezor.md b/docs/trezor.md deleted file mode 100644 index 85e1b7e8f..000000000 --- a/docs/trezor.md +++ /dev/null @@ -1,27 +0,0 @@ -# Trezor - -The Trezor is partially supported by HWI - -Current implemented commands are: - -* `getmasterxpub` -* `signtx` (with some caveats) -* `getxpub` -- `displayaddress` -- `setup` -- `wipe` -- `restore` -- `backup` -- `togglepassphrase` - -## `signtx` Caveats - -Due to the limitations of the Trezor, some transactions cannot be signed by a Trezor. - -- Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. -* Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. -* Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. - -## Note on `backup` - -Once the device is backed up at setup, the seed words will not be shown again to be backed up. The implementation here lets users know that `backup` does not work. diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index e21d335de..9499ffb28 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -1,5 +1,9 @@ -# Trezor interaction script +""" +Trezor Devices +************** +""" +from functools import wraps from typing import ( Any, Callable, @@ -124,6 +128,7 @@ def parse_multisig(script: bytes) -> Tuple[bool, Optional[messages.MultisigRedee def trezor_exception(f: Callable[..., Any]) -> Any: + @wraps(f) def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) @@ -283,6 +288,13 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: @trezor_exception def sign_tx(self, tx: PSBT) -> PSBT: + """ + Sign a transaction with the Trezor. There are some limitations to what transactions can be signed. + + - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. + - Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. + - Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. + """ self._check_unlocked() # Get this devices master key fingerprint @@ -630,6 +642,11 @@ def restore_device(self, label: str = "", word_count: int = 24) -> bool: return True def backup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + Trezor devices do not support backing up via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The {} does not support creating a backup via software'.format(self.type)) @trezor_exception From b23a4d3f91bb861759fbefe4dd839842f27514a0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:27:38 -0500 Subject: [PATCH 328/634] Move ledger docs to docstring --- docs/ledger.md | 30 ------------------------- hwilib/devices/ledger.py | 47 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 31 deletions(-) delete mode 100644 docs/ledger.md diff --git a/docs/ledger.md b/docs/ledger.md deleted file mode 100644 index 332d0067c..000000000 --- a/docs/ledger.md +++ /dev/null @@ -1,30 +0,0 @@ -# Ledger Nano X - -Currently identical to Nano S. - -# Ledger Nano S - -The Ledger Nano S is supported by HWI. -Note that the Bitcoin App must be installed and running on the device. - -Currently implemented commands: - -* `getmasterxpub` -* `signtx` (with some caveats) -* `getxpub` -* `signmessage` -- `displayaddress` -- `setup` -- `wipe` -- `restore` -- `backup` - -## `signtx` Caveats - -Due to device limitations, not all kinds of transactions can be signed by a Ledger Nano S or X. - -* Transactions containing both segwit and non-segwit inputs are not entirely supported; only the segwit inputs will be signed in this case. - -## Notes on `setup`, `wipe`, `restore`, and `backup` - -The Ledger does not allow you to setup, wipe, restore, or backup it via software. That is done on the device itself. The implementation here is just to let users know those commands do not work. diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 8f69e9a9b..935ed276f 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -1,5 +1,9 @@ -# Ledger interaction script +""" +Ledger Devices +************** +""" +from functools import wraps from typing import ( Any, Callable, @@ -93,6 +97,7 @@ def check_keypath(key_path: str) -> bool: ] def ledger_exception(f: Callable[..., Any]) -> Any: + @wraps(f) def func(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) @@ -170,6 +175,11 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey: @ledger_exception def sign_tx(self, tx: PSBT) -> PSBT: + """ + Sign a transaction with a Ledger device. Not all transactiosn can be signed by a Ledger. + + - Transactions containing both segwit and non-segwit inputs are not entirely supported; only the segwit inputs wil lbe signed in this case. + """ c_tx = CTransaction(tx.tx) tx_bytes = c_tx.serialize_with_witness() @@ -354,27 +364,62 @@ def display_multisig_address( raise BadArgumentError("The Ledger Nano S and X do not support P2SH address display") def setup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + The Coldcard does not support setup via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not support software setup') def wipe_device(self) -> bool: + """ + The Coldcard does not support wiping via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not support wiping via software') def restore_device(self, label: str = "", word_count: int = 24) -> bool: + """ + The Coldcard does not support restoring via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not support restoring via software') def backup_device(self, label: str = "", passphrase: str = "") -> bool: + """ + The Coldcard does not support backing up via software. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not support creating a backup via software') def close(self) -> None: self.dongle.close() def prompt_pin(self) -> bool: + """ + The Coldcard does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') def send_pin(self, pin: str) -> bool: + """ + The Coldcard does not need a PIN sent from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not need a PIN sent from the host') def toggle_passphrase(self) -> bool: + """ + The Coldcard does not support toggling passphrase from the host. + + :raises UnavailableActionError: Always, this function is unavailable + """ raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host') def enumerate(password: str = '') -> List[Dict[str, Any]]: From 6f95c027b0b7aa93d663974d6eb2f8701062d6e6 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:33:26 -0500 Subject: [PATCH 329/634] Move bitbox02 docs to docstring --- docs/bitbox02.md | 48 -------------------------------------- hwilib/devices/bitbox02.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 48 deletions(-) delete mode 100644 docs/bitbox02.md diff --git a/docs/bitbox02.md b/docs/bitbox02.md deleted file mode 100644 index 85cb4b46c..000000000 --- a/docs/bitbox02.md +++ /dev/null @@ -1,48 +0,0 @@ -# BitBox02 - -The BitBox02 is supported by HWI. - -Current implemented commands are: - -* `signtx` -* `getxpub` -* `displayaddress` -* `setup` -* `wipe` -* `restore` -* `backup` -* `togglepassphrase` - -# Usage Notes - -## Strict keypaths - -The BitBox02 has strict keypath validation. - -The only accepted keypaths for xpubs are (as of firmware v9.4.0): - -- `m/49'/0'/` for `p2wpkh-p2sh` (segwit wrapped in P2SH) -- `m/84'/0'/` for `p2wpkh` (native segwit v0) -- `m/48'/0'//2'` for p2wsh multisig (native segwit v0 multisig). -- `m/48'/0'//1'` for p2wsh-p2sh multisig (p2sh-wrapped segwit v0 multisig). -- `m/48'/0'/` for p2wsh and p2wsh-p2sh multisig. - -`account'` can be between `0'` and `99'`. - -For address keypaths, append `/0/
` for a receive and `/1/` for a change -address. Up to `10000` addresses are supported. - -In `--chain test` mode, the second element must be `1'` (e.g. `m/49'/1'/...`). - -## Signing with mixed input types - -The BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as -long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and -all account indexes are the same. - -Multisig and singlesig inputs cannot be mixed. - -## getmasterxpub and legacy addresses not supported - -`getmasterxpub` is the same as `getxpub` at the legacy keypath `m/44'/0'/0'`. Legacy xpub, addresses -and inputs are not supported. diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 548d07fef..adf93cb29 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -1,3 +1,8 @@ +""" +BitBox02 +******** +""" + from typing import ( cast, Any, @@ -347,6 +352,28 @@ def _get_xpub(self, keypath: Sequence[int]) -> str: ) def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey: + """ + Fetch the public key at the derivation path. + + The BitBox02 has strict keypath validation. + + The only accepted keypaths for xpubs are (as of firmware v9.4.0): + + - `m/49'/0'/` for `p2wpkh-p2sh` (segwit wrapped in P2SH) + - `m/84'/0'/` for `p2wpkh` (native segwit v0) + - `m/48'/0'//2'` for p2wsh multisig (native segwit v0 multisig). + - `m/48'/0'//1'` for p2wsh-p2sh multisig (p2sh-wrapped segwit v0 multisig). + - `m/48'/0'/` for p2wsh and p2wsh-p2sh multisig. + + `account'` can be between `0'` and `99'`. + + For address keypaths, append `/0/
` for a receive and `/1/` for a change + address. Up to `10000` addresses are supported. + + In testnet mode, the second element must be `1'` (e.g. `m/49'/1'/...`). + + Public keys for the Legacy address type (i.e. P2WPKH and P2SH multisig) derivation path is unsupported. + """ path_uint32s = parse_path(bip32_path) try: xpub_str = self._get_xpub(path_uint32s) @@ -489,6 +516,15 @@ def display_multisig_address( @bitbox02_exception def sign_tx(self, psbt: PSBT) -> PSBT: + """ + Sign a transaction with the BitBox02. + + he BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as + long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and + all account indexes are the same. + + Transactions with legacy inputs are not supported. + """ def find_our_key( keypaths: Dict[bytes, KeyOriginInfo] ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: From e3a04088ceba45480b52e1c9451229c3e092090f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 16:28:33 -0400 Subject: [PATCH 330/634] Move examples.md to examples.rst --- docs/examples.md | 145 ------------------------------------------ docs/examples.rst | 156 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 145 deletions(-) delete mode 100644 docs/examples.md create mode 100644 docs/examples.rst diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index 30b4a1443..000000000 --- a/docs/examples.md +++ /dev/null @@ -1,145 +0,0 @@ -# Examples - -Example using a Ledger Nano S: - -``` -./hwi.py enumerate -[{"type": "ledger", "path": "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0", "serial_number": "0001"}, {"type": "ledger", "path": "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@1/IOUSBHostHIDDevice@14200000,1", "serial_number": "0001"}] -``` -The OS in this case is macOS v10.13.6 (Darwin Kernel Version 17.7.0). In Linux the -"path" is shorter. - -## Extracting xpubs - -Bitcoin Core v0.17.0 and later allows you to retrieve the unspent transaction outputs (utxo) -relevant for a set of [Output Descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) with the `scantxoutset` RPC call. - -To retrieve the outputs relevant for a specific hardware wallet it is -necessary: - -1. to derive the xpub of the hardware wallet until the last hardened level - with HWI (because the private key is required) -2. to use the obtained xpub to compose the output descriptor - -These are some schemas used in hardware wallets, with the data necessary to -build the appropriate output descriptor: - -| Used schema | hardened path | further derivation | Output type | -|-------------| ------------- | -------------------|-------------| -| BIP44 | m/44h/0h/0h | /0/* and /1/* | pkh() | -| BIP49 | m/49h/0h/0h | /0/* and /1/* | sh(wpkh()) | -| BIP84 | m/84h/0h/0h | /0/* and /1/* | wpkh() | - -NOTE: -1. We could also use "combo()" in all cases as "Output Type" because it is a - "bundle" which includes pk(KEY) and pkh(KEY). If the key is compressed, it - also includes wpkh(KEY) and sh(wpkh(KEY)). - -2. It is possible to specify how many outputs to search for by setting the - maximum index of the derivation with the "range" key. In the examples - it is set to 100. - -3. The search returns zero outputs (the hardware wallet is empty). - -### [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) - -1. To obtain the xpub relative to the last hardened level (m/44h/0h/0h) - -``` -./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/44h/0h/0h -=> b'e0c4000000' -<= b'1b30010208010003'9000 -=> b'f026000000' -<= b''6d00 -=> b'e04000000d038000002c8000000080000000' -<= b'4104f4b866b49fb76529a076a1c5b25216c1f4b970cb8e3db9874beb15c5371fdb93747fde522d63be4a564dcda8a71c889f5165eac2990cafee9d416141ae8b09c722313667774c7a76697157783146317a653365676850464d58655438666a57466f4b66f9a82310c4530360ec3fee42049fbb7a3c0bfa72fdf2c5b25b09f1c3df21c938'9000 -=> b'e040000009028000002c80000000' -<= b'4104280c846650d7771396a679a55b30c558501f0b5554160c1fbd1d7301c845dacc10c256af2c8d6a13ae4a83763fa747c0d4c09cfa60bfc16714e10b0a938a4a6a2231485451557a6535486571334872553755435174564652745a535839615352674a65d62f97789c088a0b0c3ed57754f75273c6696c0d7812c702ca4f2f72c8631c04'9000 -{"xpub": "xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n"} -``` - -2. With this xpub it is possible extract the relevant UTXOs using the -`scantxoutset` RPC call in Bitcoin Core v0.17.0. - -``` -bitcoin-cli scantxoutset start '[{"desc":"pkh(xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n/0/*)","range":100}, - {"desc":"pkh(xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n/1/*)","range":100}]' -{ - "success": true, - "searched_items": 49507771, - "unspents": [ - ], - "total_amount": 0.00000000 -} -``` - -### [BIP49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) - -1. To obtain the xpub relative to the last hardened level (m/49h/0h/0h) - -``` - ./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/49h/0h/0h -=> b'e0c4000000' -<= b'1b30010208010003'9000 -=> b'f026000000' -<= b''6d00 -=> b'e04000000d03800000318000000080000000' -<= b'410437c2c1ebd83155843b3e8528b43b9786a8dc144df151b27677b76443e54b466d46b0d909d07065a2305cbba41709c78d886be37e446352186a682e9a3f9e2adc22314a594538323869434b7043576368665377396832746857377a533469486e4c444444dcdbabc6f75fbe7609bab04beb88566e3bfc98f66ab030d1af2a070f4064ec'9000 -=> b'e040000009028000003180000000' -<= b'4104c34926ea569d26e4ca06ccae25fa4332a07df69fb922a73131cfccf6a544aa3309af253eb5cee3caf8ca9a347a9e8d4429ac55b7a13f72aca36ebb51ca0f489e22314e546e3969454c587046324264664b6f326f316265785a72526e75396d65764663b310aae1803b63157ef3bb7394f985126e5f9ad4b3a6bcb118cd97875dc0e1ce'9000 -{"xpub": "xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6"} -``` -2. With this xpub it is possible extract the relevant UTXOs using the -`scantxoutset` RPC call in Bitcoin Core v0.17.0. - -``` -bitcoin-cli scantxoutset start '[{"desc":"sh(wpkh(xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6/0/*))","range":100}, - {"desc":"sh(wpkh(xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6/1/*))","range":100}]' -{ - "success": true, - "searched_items": 49507771, - "unspents": [ - ], - "total_amount": 0.00000000 -} -``` - -### [BIP84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) - -1. To obtain the xpub relative to the last hardened level (m/84h/0h/0h) - -``` - ./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/84h/0h/0h -=> b'e0c4000000' -<= b'1b30010208010003'9000 -=> b'f026000000' -<= b''6d00 -=> b'e04000000d03800000548000000080000000' -<= b'4104c79ce10d23b84ec27996e02b83964ec1953fb474ba358e70de62a09cee28dd6590f76b105fb2707c74bbefff0b4aea4156364dd813826848e8c3240d286781b722314270736737486455576a483753704535386e6d62654642773367595a554536776b2017f28f680893adfc004f5ec6db3654577c19b463326329b5d1d90de8dc24cf'9000 -=> b'e040000009028000005480000000' -<= b'410483472c03c4157d1b0f8ad98c9391dfbfc820e0180d683658ed863609da5f866aafa260048bc42cd97cb997479fd2619c5d160af68a442a80567b41fe3e763fbe22314e5531544d796971575871367278746375424a3433376d4e75736d745a73554769c03458c3a331489e3271a24a76f4ab024e040e7de7b5e88d8ce058d414f565c2'9000 -{"xpub": "xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB"} -``` - -2. With this xpub it is possible extract the relevant UTXOs using the -`scantxoutset` RPC call in Bitcoin Core v0.17.0. - -``` -bitcoin-cli scantxoutset start '[{"desc":"wpkh(xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB/0/*)","range":100}, - {"desc":"wpkh(xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB/1/*)","range":100}]' -{ - "success": true, - "searched_items": 49507771, - "unspents": [ - ], - "total_amount": 0.00000000 -} -``` - -### Binary format handling - -The input and output format supported by HWI is base64, which is prescribed by BIP174 as the string format. Note that the PSBT standard also allows for binary formatting when stored as a file. There is no direct support within HWI, but this can be easily accomplished using common utilities. A bash command-line example is detailed below, where the PSBT binary file is stored in `example.psbt` and only the common utilities `base64` and `jq` are required: - -``` -cat example.psbt | base64 --wrap=0 | ./hwi.py -t ledger --stdin signtx | jq .[] --raw-output | base64 -d > example_result.psbt -``` diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 000000000..5498222f9 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,156 @@ +Examples +******** + +Example using a Ledger Nano S:: + + ./hwi.py enumerate + [{"type": "ledger", "path": "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0", "serial_number": "0001"}, {"type": "ledger", "path": "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@1/IOUSBHostHIDDevice@14200000,1", "serial_number": "0001"}] + +The OS in this case is macOS v10.13.6 (Darwin Kernel Version 17.7.0). In Linux the +"path" is shorter. + +Extracting xpubs +================ + +Bitcoin Core v0.17.0 and later allows you to retrieve the unspent transaction outputs (utxo) +relevant for a set of `Output Descriptors `_ with the ``scantxoutset`` RPC call. + +To retrieve the outputs relevant for a specific hardware wallet it is +necessary: + +1. to derive the xpub of the hardware wallet until the last hardened level + with HWI (because the private key is required) +2. to use the obtained xpub to compose the output descriptor + +These are some schemas used in hardware wallets, with the data necessary to +build the appropriate output descriptor: + ++-------------+---------------+--------------------+-------------+ +| Used schema | hardened path | further derivation | Output type | ++=============+===============+====================+=============+ +| BIP44 | m/44h/0h/0h | /0/* and /1/* | pkh() | ++-------------+---------------+--------------------+-------------+ +| BIP49 | m/49h/0h/0h | /0/* and /1/* | sh(wpkh()) | ++-------------+---------------+--------------------+-------------+ +| BIP84 | m/84h/0h/0h | /0/* and /1/* | wpkh() | ++-------------+---------------+--------------------+-------------+ + +NOTE: + +1. We could also use "combo()" in all cases as "Output Type" because it is a + "bundle" which includes pk(KEY) and pkh(KEY). If the key is compressed, it + also includes wpkh(KEY) and sh(wpkh(KEY)). + +2. It is possible to specify how many outputs to search for by setting the + maximum index of the derivation with the "range" key. In the examples + it is set to 100. + +3. The search returns zero outputs (the hardware wallet is empty). + +`BIP44 `_ +------------------------------------------------------------------------- + +1. To obtain the xpub relative to the last hardened level (m/44h/0h/0h) + +:: + + ./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/44h/0h/0h + => b'e0c4000000' + <= b'1b30010208010003'9000 + => b'f026000000' + <= b''6d00 + => b'e04000000d038000002c8000000080000000' + <= b'4104f4b866b49fb76529a076a1c5b25216c1f4b970cb8e3db9874beb15c5371fdb93747fde522d63be4a564dcda8a71c889f5165eac2990cafee9d416141ae8b09c722313667774c7a76697157783146317a653365676850464d58655438666a57466f4b66f9a82310c4530360ec3fee42049fbb7a3c0bfa72fdf2c5b25b09f1c3df21c938'9000 + => b'e040000009028000002c80000000' + <= b'4104280c846650d7771396a679a55b30c558501f0b5554160c1fbd1d7301c845dacc10c256af2c8d6a13ae4a83763fa747c0d4c09cfa60bfc16714e10b0a938a4a6a2231485451557a6535486571334872553755435174564652745a535839615352674a65d62f97789c088a0b0c3ed57754f75273c6696c0d7812c702ca4f2f72c8631c04'9000 + {"xpub": "xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n"} + +2. With this xpub it is possible extract the relevant UTXOs using the +``scantxoutset`` RPC call in Bitcoin Core v0.17.0. + +:: + + bitcoin-cli scantxoutset start '[{"desc":"pkh(xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n/0/*)","range":100}, + {"desc":"pkh(xpub6CyidiQae2HF71YigFJqteLsRi9D1EvZJm1Lr4DWWxFVruf3vDSbfyxD9znqVkUTUzc4EdgxDRoHXn64gMbFXQGKXg5nPNfvyVcpuPNn92n/1/*)","range":100}]' + { + "success": true, + "searched_items": 49507771, + "unspents": [ + ], + "total_amount": 0.00000000 + } + +`BIP49 `_ +------------------------------------------------------------------------- + +1. To obtain the xpub relative to the last hardened level (m/49h/0h/0h) + +:: + + ./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/49h/0h/0h + => b'e0c4000000' + <= b'1b30010208010003'9000 + => b'f026000000' + <= b''6d00 + => b'e04000000d03800000318000000080000000' + <= b'410437c2c1ebd83155843b3e8528b43b9786a8dc144df151b27677b76443e54b466d46b0d909d07065a2305cbba41709c78d886be37e446352186a682e9a3f9e2adc22314a594538323869434b7043576368665377396832746857377a533469486e4c444444dcdbabc6f75fbe7609bab04beb88566e3bfc98f66ab030d1af2a070f4064ec'9000 + => b'e040000009028000003180000000' + <= b'4104c34926ea569d26e4ca06ccae25fa4332a07df69fb922a73131cfccf6a544aa3309af253eb5cee3caf8ca9a347a9e8d4429ac55b7a13f72aca36ebb51ca0f489e22314e546e3969454c587046324264664b6f326f316265785a72526e75396d65764663b310aae1803b63157ef3bb7394f985126e5f9ad4b3a6bcb118cd97875dc0e1ce'9000 + {"xpub": "xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6"} + +2. With this xpub it is possible extract the relevant UTXOs using the +``scantxoutset`` RPC call in Bitcoin Core v0.17.0. + +:: + + bitcoin-cli scantxoutset start '[{"desc":"sh(wpkh(xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6/0/*))","range":100}, + {"desc":"sh(wpkh(xpub6DP8WTA5cy2qWzdtjMUpLJHkzonepEZytzxFLMzkrcW7U4prscYnmXRQ8BesvMP3iqgQUWisAU6ipXnZw2HnNreEPYJW6TUCAfmwJPyYgG6/1/*))","range":100}]' + { + "success": true, + "searched_items": 49507771, + "unspents": [ + ], + "total_amount": 0.00000000 + } + +`BIP84 `_ +------------------------------------------------------------------------- + +1. To obtain the xpub relative to the last hardened level (m/84h/0h/0h) + +:: + + ./hwi.py -t "ledger" -d "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS02@14200000/Nano S@14200000/Nano S@0/IOUSBHostHIDDevice@14200000,0" getxpub m/84h/0h/0h + => b'e0c4000000' + <= b'1b30010208010003'9000 + => b'f026000000' + <= b''6d00 + => b'e04000000d03800000548000000080000000' + <= b'4104c79ce10d23b84ec27996e02b83964ec1953fb474ba358e70de62a09cee28dd6590f76b105fb2707c74bbefff0b4aea4156364dd813826848e8c3240d286781b722314270736737486455576a483753704535386e6d62654642773367595a554536776b2017f28f680893adfc004f5ec6db3654577c19b463326329b5d1d90de8dc24cf'9000 + => b'e040000009028000005480000000' + <= b'410483472c03c4157d1b0f8ad98c9391dfbfc820e0180d683658ed863609da5f866aafa260048bc42cd97cb997479fd2619c5d160af68a442a80567b41fe3e763fbe22314e5531544d796971575871367278746375424a3433376d4e75736d745a73554769c03458c3a331489e3271a24a76f4ab024e040e7de7b5e88d8ce058d414f565c2'9000 + {"xpub": "xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB"} + +2. With this xpub it is possible extract the relevant UTXOs using the +``scantxoutset`` RPC call in Bitcoin Core v0.17.0. + +:: + + bitcoin-cli scantxoutset start '[{"desc":"wpkh(xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB/0/*)","range":100}, + {"desc":"wpkh(xpub6DP9afdc7qsz7s7mwAvciAR2dV6vPC3gyiQbqKDzDcPAq3UQChKPimHc3uCYfTTkpoXdwRTFnVTBdFpM9ysbf6KV34uMqkD3zXr6FzkJtcB/1/*)","range":100}]' + { + "success": true, + "searched_items": 49507771, + "unspents": [ + ], + "total_amount": 0.00000000 + } + +Binary format handling +====================== + +The input and output format supported by HWI is base64, which is prescribed by BIP174 as the string format. Note that the PSBT standard also allows for binary formatting when stored as a file. There is no direct support within HWI, but this can be easily accomplished using common utilities. A bash command-line example is detailed below, where the PSBT binary file is stored in ``example.psbt`` and only the common utilities ``base64`` and ``jq`` are required: + +:: + + cat example.psbt | base64 --wrap=0 | ./hwi.py -t ledger --stdin signtx | jq .[] --raw-output | base64 -d > example_result.psbt From 436e89e8d6678d3ced24ac2d345d2673dd06bd17 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 16:31:17 -0400 Subject: [PATCH 331/634] Move release-process.md to release-process.rst --- docs/release-process.md | 51 ---------------------------------------- docs/release-process.rst | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 51 deletions(-) delete mode 100644 docs/release-process.md create mode 100644 docs/release-process.rst diff --git a/docs/release-process.md b/docs/release-process.md deleted file mode 100644 index ce8d14ccc..000000000 --- a/docs/release-process.md +++ /dev/null @@ -1,51 +0,0 @@ -# Release Process - -1. Bump version number in `pyproject.toml` and `hwilib/__init__.py`, generate the setup.py file, and git tag release -2. Build distribution archives for PyPi with `contrib/build_dist.sh` -3. For MacOS and Linux, use `contrib/build_bin.sh`. This needs to be run on a MacOS machine for the MacOS binary and on a Linux machine for the linux one. -4. For Windows, use `contrib/build_wine.sh` to build the Windows binary using wine -5. Upload distribution archives to PyPi -6. Upload distribution archives and standalone binaries to Github - -## Deterministic builds with Docker - -Create the docker image: - -``` -docker build --no-cache -t hwi-builder -f contrib/build.Dockerfile . -``` - -Build everything - -``` -docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh && contrib/build_wine.sh" -``` - -## Building macOS binary - -Note that the macOS build is non-deterministic. - -First install [pyenv](https://github.com/pyenv/pyenv) using whichever method you prefer. - -Then a deterministic build of Python 3.6.12 needs to be installed. This can be done with the patch in `contrib/reproducible-python.diff`. First `cd` into HWI's source tree. Then use: - -``` -cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.12 -``` - -Make sure that python 3.6.12 is active - -``` -$ python --version -Python 3.6.12 -``` - -Now install [Poetry](https://github.com/sdispater/poetry) with `pip install poetry` - -Additional dependencies can be installed with: - -``` -brew install libusb -``` - -Build the binaries by using `contrib/build_bin.sh`. diff --git a/docs/release-process.rst b/docs/release-process.rst new file mode 100644 index 000000000..05e202369 --- /dev/null +++ b/docs/release-process.rst @@ -0,0 +1,44 @@ +Release Process +*************** + +1. Bump version number in ``pyproject.toml`` and ``hwilib/__init__.py``, generate the setup.py file, and git tag release +2. Build distribution archives for PyPi with ``contrib/build_dist.sh`` +3. For MacOS and Linux, use ``contrib/build_bin.sh``. This needs to be run on a MacOS machine for the MacOS binary and on a Linux machine for the linux one. +4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine +5. Upload distribution archives to PyPi +6. Upload distribution archives and standalone binaries to Github + +Deterministic builds with Docker +================================ + +Create the docker image:: + + docker build --no-cache -t hwi-builder -f contrib/build.Dockerfile . + +Build everything:: + + docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh && contrib/build_wine.sh" + +Building macOS binary +===================== + +Note that the macOS build is non-deterministic. + +First install `pyenv `_ using whichever method you prefer. + +Then a deterministic build of Python 3.6.8 needs to be installed. This can be done with the patch in ``contrib/reproducible-python.diff``. First ``cd`` into HWI's source tree. Then use:: + + cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8 + +Make sure that python 3.6.8 is active:: + + $ python --version + Python 3.6.8 + +Now install `Poetry `_ with ``pip install poetry`` + +Additional dependencies can be installed with:: + + brew install libusb + +Build the binaries by using ``contrib/build_bin.sh``. From 55637cac2eb89cb8be2b67c9b4850b25c7e58e1d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 17:01:34 -0400 Subject: [PATCH 332/634] Add device support matrix doc and device notes --- README.md | 32 +----------------- docs/conf.py | 5 +++ docs/devices/index.rst | 73 ++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 4 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 docs/devices/index.rst diff --git a/README.md b/README.md index 4b0168d40..08f019767 100644 --- a/README.md +++ b/README.md @@ -86,37 +86,7 @@ pass the `--help` parameter after the command name; for example: ## Device Support -The below table lists what devices and features are supported for each device. - -Please also see [docs](docs/) for additional information about each device. - -* `✓` - supported by the firmware and implemented in HWI -* `✗` - supported by the firmware and not implemented in HWI -* `-` - not supported by the firmware - -| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard | -|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| Support Planned | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Implemented | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| xpub retrieval | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Message Signing | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | -| Device Setup | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| Device Wipe | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| Device Recovery | - | - | ✓ | ✓ | - | ✓ | ✓ | - | -| Device Backup | - | - | - | - | ✓ | ✓ | - | ✓ | -| P2PKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | -| P2SH-P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| P2SH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| P2SH-P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Bare Multisig Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | -| Arbitrary scriptPubKey Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | -| Arbitrary redeemScript Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | -| Arbitrary witnessScript Inputs | ✓ | ✓ | - | - | ✓ | - | - | - | -| Non-wallet inputs | ✓ | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | -| Mixed Segwit and Non-Segwit Inputs | - | - | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Display on device screen | ✓ | ✓ | ✓ | ✓ | - | ✓ | ✓ | ✓ | +For documentation on devices supported and how they are supported, please check the [devicesupport page](docs/devices/index.rst) ## Using with Bitcoin Core diff --git a/docs/conf.py b/docs/conf.py index 6a9653ae6..db6fadfca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,6 +31,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. @@ -41,6 +42,10 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +# Autodoc options +autodoc_default_options = { + "inherited-members": True, +} # -- Options for HTML output ------------------------------------------------- diff --git a/docs/devices/index.rst b/docs/devices/index.rst new file mode 100644 index 000000000..7286d9cf9 --- /dev/null +++ b/docs/devices/index.rst @@ -0,0 +1,73 @@ +Supported Devices +***************** + +Support Matrix +============== + +The table below lists what devices and features are supported for each device. + +* ``✓`` - supported by the firmware and implemented in HWI +* ``✗`` - supported by the firmware and not implemented in HWI +* ``―`` - not supported by the firmware + ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Feature \\ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard | ++====================================+===============+===============+============+================+==========+==========+=========+==========+ +| Support Planned | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Implemented | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| xpub retrieval | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Message Signing | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Device Setup | ― | ― | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Device Wipe | ― | ― | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Device Recovery | ― | ― | ✓ | ✓ | ― | ✓ | ✓ | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Device Backup | ― | ― | ― | ― | ✓ | ✓ | ― | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2PKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2SH-P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2WPKH Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2SH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2SH-P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| P2WSH Multisig Inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Bare Multisig Inputs | ✓ | ✓ | ― | ― | ✓ | ― | ― | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Arbitrary scriptPubKey Inputs | ✓ | ✓ | ― | ― | ✓ | ― | ― | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Arbitrary redeemScript Inputs | ✓ | ✓ | ― | ― | ✓ | ― | ― | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Arbitrary witnessScript Inputs | ✓ | ✓ | ― | ― | ✓ | ― | ― | ― | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Non-wallet inputs | ✓ | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Mixed Segwit and Non-Segwit Inputs | ― | ― | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +| Display on device screen | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ✓ | ++------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ + +Device APIs +=========== + +.. automodule:: hwilib.devices.ledger + :members: +.. automodule:: hwilib.devices.trezor + :members: +.. automodule:: hwilib.devices.digitalbitbox + :members: +.. automodule:: hwilib.devices.bitbox02 + :members: +.. automodule:: hwilib.devices.keepkey + :members: +.. automodule:: hwilib.devices.coldcard + :members: diff --git a/docs/index.rst b/docs/index.rst index ee29433f6..f7dc270d5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Welcome to Hardware Wallet Interface's documentation! :maxdepth: 2 :caption: Contents: + devices/index Indices and tables From bd8a4100e44abf7d54c527f1cd7cf29ed5d83834 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 17:08:44 -0400 Subject: [PATCH 333/634] Move bitcoin-core-usage.rst and examples.rst to doc/examples/ --- README.md | 2 +- docs/{ => examples}/bitcoin-core-usage.rst | 0 docs/{ => examples}/examples.rst | 0 docs/examples/index.rst | 8 ++++++++ docs/index.rst | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) rename docs/{ => examples}/bitcoin-core-usage.rst (100%) rename docs/{ => examples}/examples.rst (100%) create mode 100644 docs/examples/index.rst diff --git a/README.md b/README.md index 08f019767..42d59618e 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ For documentation on devices supported and how they are supported, please check ## Using with Bitcoin Core -See [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md). +See [Using Bitcoin Core with Hardware Wallets](docs/examples/bitcoin-core-usage.rst). ## License diff --git a/docs/bitcoin-core-usage.rst b/docs/examples/bitcoin-core-usage.rst similarity index 100% rename from docs/bitcoin-core-usage.rst rename to docs/examples/bitcoin-core-usage.rst diff --git a/docs/examples.rst b/docs/examples/examples.rst similarity index 100% rename from docs/examples.rst rename to docs/examples/examples.rst diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 000000000..2d3fc41b0 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,8 @@ +Example Usage +============= + +.. toctree:: + :maxdepth: 1 + + bitcoin-core-usage + examples diff --git a/docs/index.rst b/docs/index.rst index f7dc270d5..4a7f3f12c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ Welcome to Hardware Wallet Interface's documentation! :caption: Contents: devices/index + examples/index Indices and tables From bfbe83fd8ed7c949c5d50008908bf2b88c88a5ca Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 17:09:24 -0400 Subject: [PATCH 334/634] Move release-process.rst to docs/development/ --- docs/development/index.rst | 7 +++++++ docs/{ => development}/release-process.rst | 0 docs/index.rst | 1 + 3 files changed, 8 insertions(+) create mode 100644 docs/development/index.rst rename docs/{ => development}/release-process.rst (100%) diff --git a/docs/development/index.rst b/docs/development/index.rst new file mode 100644 index 000000000..99931a498 --- /dev/null +++ b/docs/development/index.rst @@ -0,0 +1,7 @@ +Development +*********** + +.. toctree:: + :caption: Contents: + + release-process diff --git a/docs/release-process.rst b/docs/development/release-process.rst similarity index 100% rename from docs/release-process.rst rename to docs/development/release-process.rst diff --git a/docs/index.rst b/docs/index.rst index 4a7f3f12c..ec7778069 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ Welcome to Hardware Wallet Interface's documentation! :caption: Contents: devices/index + development/index examples/index From 41e2457028801059466ef8ea1297416464f8a1b0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 18:03:47 -0400 Subject: [PATCH 335/634] Add docs/usage and installation docs --- docs/index.rst | 1 + docs/usage/index.rst | 7 ++++++ docs/usage/installation.rst | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 docs/usage/index.rst create mode 100644 docs/usage/installation.rst diff --git a/docs/index.rst b/docs/index.rst index ec7778069..5b5039ebb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Welcome to Hardware Wallet Interface's documentation! :maxdepth: 2 :caption: Contents: + usage/index devices/index development/index examples/index diff --git a/docs/usage/index.rst b/docs/usage/index.rst new file mode 100644 index 000000000..763d1df19 --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,7 @@ +Usage +***** + +.. toctree:: + :maxdepth: 2 + + installation diff --git a/docs/usage/installation.rst b/docs/usage/installation.rst new file mode 100644 index 000000000..579173672 --- /dev/null +++ b/docs/usage/installation.rst @@ -0,0 +1,44 @@ +Installation +************ + +HWI is distributed in 2 different ways: + +1. Self-contained executable binaries +2. Python package + +Binaries +======== + +The self-contained binaries are availabe for download from the `releases page `_. + +Download and extract the package for your operating system and architecture. +The ``hwi`` binary (``hwi.exe`` for Windows) is a command line tool and executed from the terminal (command prompt in Windows). +The ``hwi-qt`` binary (``hwi-qt.exe`` for Windows) is a GUI tool and can be executed as any typical application. + +Python Package +============== + +The python packages are distributed both from the `releases page `_ and from `PyPi `_. + +In either case, make sure that you have installed ``pip`` and that it is update to date. + +From Releases +------------- + +Download either the Python wheel ``hwi--py3-none-any.whl`` or the source package ``hwi-.tar.gz``. +It is recommended to use the wheel over the source package unless your Python installation does not support wheels. + +Install the downloaded file using ``pip``. For example:: + + pip install hwi-1.1.2-py3-none-any.whl + +or:: + + pip install hwi-1.1.2.tar.gz + +From PyPI +--------- + +As HWI is also uploaded to PyPi, it can be installed with:: + + pip install hwi From 01d7644153ac8b45138d4397f5a2386ec1c7ded3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:50:27 -0500 Subject: [PATCH 336/634] Docstring hwilib/commands.py --- hwilib/commands.py | 221 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 1 deletion(-) diff --git a/hwilib/commands.py b/hwilib/commands.py index ce25c3ebd..a6f762cdf 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -1,6 +1,22 @@ #! /usr/bin/env python3 -# Hardware wallet interaction script +""" +Commands +******** + +The functions in this module are the primary way to interact with hardware wallets. +Each function that takes a ``client`` uses a :class:`~hwilib.hwwclient.HardwareWalletClient`. +The functions then call public members of that client to retrieve the data needed. + +Clients can be constructed using :func:`~find_device` or :func:`~get_client`. + +The :func:`~enumerate` function returns information about what devices are available to be connected to. +These information can then be used with :func:`~find_device` or :func:`~get_client` to get a :class:`~hwilib.hwwclient.HardwareWalletClient`. + +Note that this documentation does not specify every exception that can be raised. +Many exceptions are buried within the functions implemented by each device's :class:`~hwilib.hwwclient.HardwareWalletClient`. +For more information about the exceptions that those can raise, please see the specific client documentation. +""" import importlib import platform @@ -51,6 +67,17 @@ # Get the client for the device def get_client(device_type: str, device_path: str, password: str = "", expert: bool = False) -> Optional[HardwareWalletClient]: + """ + Returns a HardwareWalletClient for the given device type at the device path + + :param device_type: The type of device + :param device_path: The path specifying where the device can be accessed as returned by :func:`~enumerate` + :param password: The password to use for this device + :param expert: Whether the device should be opened in expert mode (prints more information for some commands) + :return: A :class:`~hwilib.hwwclient.HardwareWalletClient` to interact with the device + :raises: UnknownDeviceError: if the device type is not known by HWI + """ + device_type = device_type.split('_')[0] class_name = device_type.capitalize() module = device_type.lower() @@ -69,6 +96,13 @@ def get_client(device_type: str, device_path: str, password: str = "", expert: b # Get a list of all available hardware wallets def enumerate(password: str = "") -> List[Dict[str, Any]]: + """ + Enumerate all of the devices that HWI can potentially access. + + :param password: The password to use for devices which take passwords from the host. + :return: A list of devices for which clients can be created for. + """ + result: List[Dict[str, Any]] = [] for module in all_devs: @@ -86,6 +120,20 @@ def find_device( fingerprint: Optional[str] = None, expert: bool = False, ) -> Optional[HardwareWalletClient]: + """ + Find a device from the device type or fingerprint and get a client to access it. + This is used as an alternative to :func:`~get_client` if the device path is not known. + + :param password: A password that may be needed to access the device if it can take passwords from the host + :param device_type: The type of device. The client returned will be for this type of device. + If not provided, the fingerprint must be provided + :param fingerprint: The fingerprint of the master public key for the device. + The client returned will have a master public key fingerprint matching this. + If not provided, device_type must be provided. + :param expert: Whether the device should be opened in expert mode (enables additional output for some actions) + :return: A client to interact with the found device + """ + devices = enumerate(password) for d in devices: if device_type is not None and d['type'] != device_type and d['model'] != device_type: @@ -114,15 +162,40 @@ def find_device( return None def getmasterxpub(client: HardwareWalletClient) -> Dict[str, str]: + """ + Get the master extended public key from a client + + :param client: The client to interact with + :return: A dictionary containing the public key at the ``m/44'/0'/0'`` derivation path. + Returned as ``{"xpub": }``. + """ return {"xpub": client.get_master_xpub().to_string()} def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, str]: + """ + Sign a Partially Signed Bitcoin Transaction (PSBT) with the client. + + :param client: The client to interact with + :param psbt: The PSBT to sign + :return: A dictionary containing the processed PSBT serialized in Base64. + Returned as ``{"psbt": }``. + """ # Deserialize the transaction tx = PSBT() tx.deserialize(psbt) return {"psbt": client.sign_tx(tx).serialize()} def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, Any]: + """ + Get the master public key at a path from a client + + :param client: The client to interact with + :param path: The derivation path for the public key to retrieve + :param expert: Whether to provide more information intended for experts. + :return: A dictionary containing the public key at the ``bip32_path``. + With expert mode, the information contained within the xpub are decoded and displayed. + Returned as ``{"xpub": }``. + """ xpub = client.get_pubkey_at_path(path) result: Dict[str, Any] = {"xpub": xpub.to_string()} if expert: @@ -130,6 +203,18 @@ def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Di return result def signmessage(client: HardwareWalletClient, message: str, path: str) -> Dict[str, str]: + """ + Sign a message using the key at the derivation path with the client. + + The message will be signed using the Bitcoin signed message standard used by Bitcoin Core. + The message can be either a string which is then encoded to bytes, or bytes. + + :param client: The client to interact with + :param message: The message to sign + :param path: The derivation path for the key to sign with + :return: A dictionary containing the signature. + Returned as ``{"signature": }``. + """ return {"signature": client.sign_message(message, path)} def getkeypool_inner( @@ -142,6 +227,19 @@ def getkeypool_inner( account: int = 0, addr_type: AddressType = AddressType.WPKH ) -> List[Dict[str, Any]]: + """ + :meta private: + + Construct a single dictionary that specifies a single descriptor and the extra fields needed for ``importmulti`` or ``importdescriptors`` to import it. + + :param path: The derivation path for the key in the descriptor + :param start: The start index of the range, inclusive + :param end: The end index of the range, inclusive + :param internal: Whether to specify this import is change + :param keypool: Whether to specify this import should be added to the keypool + :param account: The BIP 44 account to use if ``path`` is not specified + :param addr_type: The type of address the descriptor should create + """ master_fpr = client.get_master_fingerprint() desc = getdescriptor(client, master_fpr, path, internal, addr_type, account, start, end) @@ -170,6 +268,20 @@ def getdescriptor( start: Optional[int] = None, end: Optional[int] = None ) -> Descriptor: + """ + Get a descriptor from the client. + + :param client: The client to interact with + :param master_fpr: The hex string for the master fingerprint of the device to use in the descriptor + :param path: The derivation path for the xpub from which additional keys will be derived. + :param internal: Whether the dictionary should indicate that the descriptor should be for change addresses + :param addr_type: The type of address the descriptor should create + :param account: The BIP 44 account to use if ``path`` is not specified + :param start: The start of the range to import, inclusive + :param end: The end of the range to import, inclusive + :return: The descriptor constructed given the above arguments and key fetched from the device + :raises: BadArgumentError: if an argument is malformed or missing. + """ is_wpkh = addr_type is AddressType.WPKH is_sh_wpkh = addr_type is AddressType.SH_WPKH @@ -244,6 +356,23 @@ def getkeypool( addr_type: AddressType = AddressType.PKH, addr_all: bool = False ) -> List[Dict[str, Any]]: + """ + Get a dictionary which can be passed to Bitcoin Core's ``importmulti`` or ``importdescriptors`` RPCs to import a watchonly wallet based on the client. + By default, a descriptor for legacy addresses is returned. + + :param client: The client to interact with + :param path: The derivation path for the xpub from which additional keys will be derived. + :param start: The start of the range to import, inclusive + :param end: The end of the range to import, inclusive + :param internal: Whether the dictionary should indicate that the descriptor should be for change addresses + :param keypool: Whether the dictionary should indicate that the dsecriptor should be added to the Bitcoin Core keypool/addresspool + :param account: The BIP 44 account to use if ``path`` is not specified + :param sh_wpkh: Whether to return a descriptor specifying p2sh-segwit addresses + :param wpkh: Whether to return a descriptor specifying native segwit addresses + :param addr_all: Whether to return a multiple descriptors for every address type + :return: The dictionary containing the descriptor and all of the arguments for ``importmulti`` or ``importdescriptors`` + :raises: BadArgumentError: if an argument is malformed or missing. + """ addr_types = [addr_type] if addr_all: @@ -265,6 +394,14 @@ def getdescriptors( client: HardwareWalletClient, account: int = 0 ) -> Dict[str, List[str]]: + """ + Get descriptors from the client. + + :param client: The client to interact with + :param account: The BIP 44 account to use + :return: Multiple descriptors from the device matching the BIP 44 standard paths and the given ``account``. + :raises: BadArgumentError: if an argument is malformed or missing. + """ master_fpr = client.get_master_fingerprint() result = {} @@ -293,6 +430,18 @@ def displayaddress( desc: Optional[str] = None, addr_type: AddressType = AddressType.PKH ) -> Dict[str, str]: + """ + Display an address on the device for client. + The address can be specified by the path with additional parameters, or by a descriptor. + + :param client: The client to interact with + :param path: The path of the address to display. Mutually exclusive with ``desc`` + :param desc: The descriptor to display the address for. Mutually exclusive with ``path`` + :param addr_type: The address type to return. Only works with ``path`` + :return: A dictionary containing the address displayed. + Returned as ``{"address": }``. + :raises: BadArgumentError: if an argument is malformed, missing, or conflicts. + """ if path is not None: return {"address": client.display_singlesig_address(path, addr_type)} elif desc is not None: @@ -331,27 +480,97 @@ def displayaddress( raise BadArgumentError("Missing both path and descriptor") def setup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: + """ + Setup a device that has not yet been initialized. + + :param client: The client to interact with + :param label: The label to apply to the newly setup device + :param backup_passphrase: The passphrase to use for the backup, if backups are encrypted for that device + :return: A dictionary with the ``success`` key. + """ return {"success": client.setup_device(label, backup_passphrase)} def wipe_device(client: HardwareWalletClient) -> Dict[str, bool]: + """ + Wipe a device + + :param client: The client to interact with + :return: A dictionary with the ``success`` key. + """ return {"success": client.wipe_device()} def restore_device(client: HardwareWalletClient, label: str = "", word_count: int = 24) -> Dict[str, bool]: + """ + Restore a backup to a device that has not yet been initialized. + + :param client: The client to interact with + :param label: The label to apply to the newly setup device + :param word_count: The number of words in the recovery phrase + :return: A dictionary with the ``success`` key. + """ return {"success": client.restore_device(label, word_count)} def backup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]: + """ + Create a backup of the device + + :param client: The client to interact with + :param label: The label to apply to the newly setup device + :param backup_passphrase: The passphrase to use for the backup, if backups are encrypted for that device + :return: A dictionary with the ``success`` key. + """ return {"success": client.backup_device(label, backup_passphrase)} def prompt_pin(client: HardwareWalletClient) -> Dict[str, bool]: + """ + Trigger the device to show the setup for PIN entry. + + :param client: The client to interact with + :return: A dictionary with the ``success`` key. + """ return {"success": client.prompt_pin()} def send_pin(client: HardwareWalletClient, pin: str) -> Dict[str, bool]: + """ + Send a PIN to the device after :func:`prompt_pin` has been called. + + :param client: The client to interact with + :param pin: The PIN to send + :return: A dictionary with the ``success`` key. + """ return {"success": client.send_pin(pin)} def toggle_passphrase(client: HardwareWalletClient) -> Dict[str, bool]: + """ + Toggle whether the device is using a BIP 39 passphrase. + + :param client: The client to interact with + :return: A dictionary with the ``success`` key. + """ return {"success": client.toggle_passphrase()} def install_udev_rules(source: str, location: str) -> Dict[str, bool]: + """ + Install the udev rules to the local machine. + The rules will be copied from the source to the location. + ``udevadm`` will also be triggered and the rules reloaded so that the devices can be plugged in and used immediately. + A ``plugdev`` group will also be created if it does not exist and the user will be added to it. + + The recommended source location is ``hwilib/udev``. The recommended destination location is ``/etc/udev/rules.d`` + + This function is equivalent to:: + + sudo cp hwilib/udev/*rules /etc/udev/rules.d/ + sudo udevadm trigger + sudo udevadm control --reload-rules + sudo groupadd plugdev + sudo usermod -aG plugdev `whoami` + + :param source: The directory containing the udev rules to install + :param location: The directory to install the udev rules to + :return: A dictionary with the ``success`` key. + :raises: NotImplementedError: if udev rules cannot be installed on this system, i.e. it is not linux. + """ if platform.system() == "Linux": from .udevinstaller import UDevInstaller return {"success": UDevInstaller.install(source, location)} From 0277e5f3e3c7521754e8c112f22355a20e8a78b4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 16:54:04 -0500 Subject: [PATCH 337/634] Add docstrings to udevinstaller.py --- hwilib/udevinstaller.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/hwilib/udevinstaller.py b/hwilib/udevinstaller.py index cf7af119b..4c32437b0 100644 --- a/hwilib/udevinstaller.py +++ b/hwilib/udevinstaller.py @@ -1,3 +1,10 @@ +""" +UDev Rules Installer +******************** + +Classes and utilities for installing device udev rules. +""" + from .errors import NeedsRootError from subprocess import check_call, CalledProcessError, DEVNULL @@ -5,8 +12,20 @@ from os import path, listdir, getlogin, geteuid class UDevInstaller(object): + """ + Installs the udev rules + """ @staticmethod def install(source: str, location: str) -> bool: + """ + Install the udev rules from source into location. + This will also reload and trigger udevadm so that devices matching the new rules will be detected. + The user will be added to the ``plugdev`` group. If the group doesn't exist, the user will be added to it. + + :param source: The path to the source directory containing the rules + :param location: The path to the directory to copy the rules to + :return: Whether the install was successful + """ try: udev_installer = UDevInstaller() udev_installer.copy_udev_rule_files(source, location) @@ -29,12 +48,21 @@ def _execute(self, cmd: str, *args: str) -> None: check_call(command, stderr=DEVNULL, stdout=DEVNULL) def trigger(self) -> None: + """ + Run ``udevadm trigger`` + """ self._execute(self._udevadm, 'trigger') def reload_rules(self) -> None: + """ + Run ``udevadm control --reload-rules`` + """ self._execute(self._udevadm, 'control', '--reload-rules') def add_user_plugdev_group(self) -> None: + """ + Add the user to the ``plugdev`` group + """ self._create_group('plugdev') self._add_user_to_group(getlogin(), 'plugdev') @@ -49,6 +77,12 @@ def _add_user_to_group(self, user: str, group: str) -> None: self._execute(self._usermod, '-aG', group, user) def copy_udev_rule_files(self, source: str, location: str) -> None: + """ + Copy the udev rules from source to location + + :param source: The path to the source directory containing the rules + :param location: The path to the directory to copy the rules to + """ src_dir_path = source for rules_file_name in listdir(_resource_path(src_dir_path)): if '.rules' in rules_file_name: From 11fe5d16bc40acc39820c1b265a86647b6853e66 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 15:03:36 -0400 Subject: [PATCH 338/634] Add public API documentation --- docs/conf.py | 3 +++ docs/usage/api-usage.rst | 19 +++++++++++++++++++ docs/usage/index.rst | 1 + 3 files changed, 23 insertions(+) create mode 100644 docs/usage/api-usage.rst diff --git a/docs/conf.py b/docs/conf.py index db6fadfca..1be465a5c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,3 +58,6 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] + +# Autodoc config to include type hints in the description +autodoc_typehints = "description" diff --git a/docs/usage/api-usage.rst b/docs/usage/api-usage.rst new file mode 100644 index 000000000..4e3da0b21 --- /dev/null +++ b/docs/usage/api-usage.rst @@ -0,0 +1,19 @@ +API Usage +========= + +The library API for use by projects importing ``hwilib`` can be found here. + +.. automodule:: hwilib.commands + :members: +.. automodule:: hwilib.errors + :members: +.. automodule:: hwilib.udevinstaller + :members: +.. automodule:: hwilib.psbt + :members: +.. automodule:: hwilib.descriptor + :members: +.. automodule:: hwilib.key + :members: +.. automodule:: hwilib.common + :members: diff --git a/docs/usage/index.rst b/docs/usage/index.rst index 763d1df19..957cda375 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -5,3 +5,4 @@ Usage :maxdepth: 2 installation + api-usage From 9cadcc8317f81d9619f805a7deb0596685dea1d0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 17:00:37 -0500 Subject: [PATCH 339/634] Add hwilib/hwwclient.py to sphinx docs --- docs/usage/api-usage.rst | 2 ++ hwilib/hwwclient.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/usage/api-usage.rst b/docs/usage/api-usage.rst index 4e3da0b21..2ed097e7e 100644 --- a/docs/usage/api-usage.rst +++ b/docs/usage/api-usage.rst @@ -3,6 +3,8 @@ API Usage The library API for use by projects importing ``hwilib`` can be found here. +.. automodule:: hwilib.hwwclient + :members: .. automodule:: hwilib.commands :members: .. automodule:: hwilib.errors diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e551e65c4..a32c2e9a4 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -1,3 +1,10 @@ +""" +Hardware Wallet Client Interface +******************************** + +The :class:`HardwareWalletClient` is the class which all of the specific device implementations subclass. +""" + from typing import ( Dict, List, @@ -18,6 +25,13 @@ class HardwareWalletClient(object): """ def __init__(self, path: str, password: str, expert: bool) -> None: + """ + :param path: Path to the device as returned by :func:`~hwilib.commands.enumerate` + :param password: A password/passphrase to use with the device. + Typically a BIP 39 passphrase, but not always. + See device specific documentation for further details. + :param expert: Whether to return additional information intended for experts. + """ self.path = path self.password = password self.message_magic = b"\x18Bitcoin Signed Message:\n" @@ -135,6 +149,11 @@ def setup_device( """ Setup the device. + :param label: A label to apply to the device. + See device specific documentation for details as to what this actually does. + :param passphrase: A passphrase to apply to the device. + Typically a BIP 39 passphrase. + See device specific documentation for details as to what this actually does. :return: Whether the setup was successful :raises UnavailableActionError: if appropriate for the device. """ @@ -147,6 +166,9 @@ def restore_device( """ Restore the device. + :param label: A label to apply to the device. + See device specific documentation for details as to what this actually does. + :param word_count: The number of BIP 39 mnemonic words. :return: Whether the restore was successful :raises UnavailableActionError: if appropriate for the device. """ @@ -159,6 +181,10 @@ def backup_device( """ Backup the device. + :param label: A label to apply to the backup. + See device specific documentation for details as to what this actually does. + :param passphrase: A passphrase to apply to the backup. + See device specific documentation for details as to what this actually does. :return: Whether the backup was successful :raises UnavailableActionError: if appropriate for the device. """ @@ -184,6 +210,7 @@ def send_pin(self, pin: str) -> bool: """ Send PIN. + :param pin: The PIN :return: Whether the PIN successfully unlocked the device :raises UnavailableActionError: if appropriate for the device. """ From b209f369778aae0b441cb096e4afb7a2ea3717b4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 16:29:35 -0400 Subject: [PATCH 340/634] docstring errors.py and add to api-usage.rst --- docs/conf.py | 6 ++ hwilib/errors.py | 157 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 143 insertions(+), 20 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1be465a5c..a25aa1429 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,3 +61,9 @@ # Autodoc config to include type hints in the description autodoc_typehints = "description" + +# Order the autodoc members by type +autodoc_member_order = "bysource" + +# Show both class and init docstring +autoclass_content = "both" diff --git a/hwilib/errors.py b/hwilib/errors.py index 069ae2d63..1f8c9195d 100644 --- a/hwilib/errors.py +++ b/hwilib/errors.py @@ -1,94 +1,203 @@ -# Defines errors and error codes +""" +Errors and Error Codes +********************** + +HWI has several possible Exceptions with corresponding error codes. + +:class:`~hwilib.hwwclient.HardwareWalletClient` functions and :mod:`~hwilib.commands` functions will generally raise an exception that is a subclass of :class:`HWWError`. +The HWI command line tool will convert these exceptions into a dictionary containing the error message and error code. +These look like ``{"error": "", "code": }``. +""" from typing import Any, Dict, Iterator, Optional from contextlib import contextmanager # Error codes -NO_DEVICE_TYPE = -1 -MISSING_ARGUMENTS = -2 -DEVICE_CONN_ERROR = -3 -UNKNWON_DEVICE_TYPE = -4 -INVALID_TX = -5 -NO_PASSWORD = -6 -BAD_ARGUMENT = -7 -NOT_IMPLEMENTED = -8 -UNAVAILABLE_ACTION = -9 -DEVICE_ALREADY_INIT = -10 -DEVICE_ALREADY_UNLOCKED = -11 -DEVICE_NOT_READY = -12 -UNKNOWN_ERROR = -13 -ACTION_CANCELED = -14 -DEVICE_BUSY = -15 -NEED_TO_BE_ROOT = -16 -HELP_TEXT = -17 -DEVICE_NOT_INITIALIZED = -18 +NO_DEVICE_TYPE = -1 #: Device type was not specified +MISSING_ARGUMENTS = -2 #: Arguments are missing +DEVICE_CONN_ERROR = -3 #: Error connecting to the device +UNKNWON_DEVICE_TYPE = -4 #: Device type is unknown +INVALID_TX = -5 #: Transaction is invalid +NO_PASSWORD = -6 #: No password provided, but one is needed +BAD_ARGUMENT = -7 #: Bad, malformed, or conflicting argument was provided +NOT_IMPLEMENTED = -8 #: Function is not implemented +UNAVAILABLE_ACTION = -9 #: Function is not available for this device +DEVICE_ALREADY_INIT = -10 #: Device is already initialized +DEVICE_ALREADY_UNLOCKED = -11 #: Device is already unlocked +DEVICE_NOT_READY = -12 #: Device is not ready +UNKNOWN_ERROR = -13 #: An unknown error occurred +ACTION_CANCELED = -14 #: Action was canceled by the user +DEVICE_BUSY = -15 #: Device is busy +NEED_TO_BE_ROOT = -16 #: User needs to be root to perform action +HELP_TEXT = -17 #: Help text was requested by the user +DEVICE_NOT_INITIALIZED = -18 #: Device is not initialized # Exceptions class HWWError(Exception): - def __init__(self, msg: str, code: int): + """ + Generic exception type produced by HWI + Subclassed by specific Errors to have Exceptions that have specific error codes. + + Contains a message and error code. + """ + def __init__(self, msg: str, code: int) -> None: + """ + Create an exception with the message and error code + + :param msg: The error message + :param code: The error code + """ Exception.__init__(self) self.code = code self.msg = msg def get_code(self) -> int: + """ + Get the error code for this Error + + :return: The error code + """ return self.code def get_msg(self) -> str: + """ + Get the error message for this Error + + :return: The error message + """ return self.msg def __str__(self) -> str: return self.msg class NoPasswordError(HWWError): + """ + :class:`HWWError` for :data:`NO_PASSWORD` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, NO_PASSWORD) class UnavailableActionError(HWWError): + """ + :class:`HWWError` for :data:`UNAVAILABLE_ACTION` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, UNAVAILABLE_ACTION) class DeviceAlreadyInitError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_ALREADY_INIT` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, DEVICE_ALREADY_INIT) class DeviceNotReadyError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_NOT_READY` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, DEVICE_NOT_READY) class DeviceAlreadyUnlockedError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_ALREADY_UNLOCKED` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, DEVICE_ALREADY_UNLOCKED) class UnknownDeviceError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_TYPE` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, UNKNWON_DEVICE_TYPE) class NotImplementedError(HWWError): + """ + :class:`HWWError` for :data:`NOT_IMPLEMENTED` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, NOT_IMPLEMENTED) class PSBTSerializationError(HWWError): + """ + :class:`HWWError` for :data:`INVALID_TX` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, INVALID_TX) class BadArgumentError(HWWError): + """ + :class:`HWWError` for :data:`BAD_ARGUMENT` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, BAD_ARGUMENT) class DeviceFailureError(HWWError): + """ + :class:`HWWError` for :data:`UNKNOWN_ERROR` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, UNKNOWN_ERROR) class ActionCanceledError(HWWError): + """ + :class:`HWWError` for :data:`ACTION_CANCELED` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, ACTION_CANCELED) class DeviceConnectionError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_CONN_ERROR` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, DEVICE_CONN_ERROR) class DeviceBusyError(HWWError): + """ + :class:`HWWError` for :data:`DEVICE_BUSY` + """ def __init__(self, msg: str): + """ + :param msg: The error message + """ HWWError.__init__(self, msg, DEVICE_BUSY) class NeedsRootError(HWWError): @@ -102,6 +211,14 @@ def handle_errors( code: int = UNKNOWN_ERROR, debug: bool = False, ) -> Iterator[None]: + """ + Context manager to catch all Exceptions and HWWErrors to return them as dictionaries containing the error message and code. + + :param msg: Error message prefix. Attached to the beginning of each error message + :param result: The dictionary to put the resulting error in + :param code: The default error code to use for Exceptions + :param debug: Whether to also print out the traceback for debugging purposes + """ if result is None: result = {} From 46687afcbfd91eb4d2a19d1c232713e09ca31932 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 19:04:21 -0400 Subject: [PATCH 341/634] Docstring descriptor.py and include in api-usage.rst --- hwilib/descriptor.py | 143 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/hwilib/descriptor.py b/hwilib/descriptor.py index dd35b6b91..6cf4ff971 100644 --- a/hwilib/descriptor.py +++ b/hwilib/descriptor.py @@ -1,3 +1,15 @@ +""" +Output Script Descriptors +************************* + +HWI has a more limited implementation of descriptors. +See `Bitcoin Core's documentation `_ for more details on descriptors. + +This implementation only supports ``sh()``, ``wsh()``, ``pkh()``, ``wpkh()``, ``multi()``, and ``sortedmulti()`` descriptors. +Descriptors can be parsed, however the actual scripts are not generated. +""" + + from .key import ExtendedKey, KeyOriginInfo, parse_path from .common import hash160, sha256 @@ -10,11 +22,15 @@ Tuple, ) -# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp ExpandedScripts = namedtuple("ExpandedScripts", ["output_script", "redeem_script", "witness_script"]) def PolyMod(c: int, val: int) -> int: + """ + :meta private: + Function to compute modulo over the polynomial used for descriptor checksums + From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp + """ c0 = c >> 35 c = ((c & 0x7ffffffff) << 5) ^ val if (c0 & 1): @@ -30,6 +46,12 @@ def PolyMod(c: int, val: int) -> int: return c def DescriptorChecksum(desc: str) -> str: + """ + Compute the checksum for a descriptor + + :param desc: The descriptor string to compute a checksum for + :return: A checksum + """ INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -59,16 +81,32 @@ def DescriptorChecksum(desc: str) -> str: return ''.join(ret) def AddChecksum(desc: str) -> str: + """ + Compute and attach the checksum for a descriptor + + :param desc: The descriptor string to add a checksum to + :return: Descriptor with checksum + """ return desc + "#" + DescriptorChecksum(desc) class PubkeyProvider(object): + """ + A public key expression in a descriptor. + Can contain the key origin info, the pubkey itself, and subsequent derivation paths for derivation from the pubkey + The pubkey can be a typical pubkey or an extended pubkey. + """ def __init__( self, origin: Optional['KeyOriginInfo'], pubkey: str, deriv_path: Optional[str] ) -> None: + """ + :param origin: The key origin if one is available + :param pubkey: The public key. Either a hex string or a serialized extended pubkey + :param deriv_path: Additional derivation path if the pubkey is an extended pubkey + """ self.origin = origin self.pubkey = pubkey self.deriv_path = deriv_path @@ -84,6 +122,12 @@ def __init__( @classmethod def parse(cls, s: str) -> 'PubkeyProvider': + """ + Deserialize a key expression from the string into a ``PubkeyProvider``. + + :param s: String containing the key expression + :return: A new ``PubkeyProvider`` containing the details given by ``s`` + """ origin = None deriv_path = None @@ -101,6 +145,11 @@ def parse(cls, s: str) -> 'PubkeyProvider': return cls(origin, pubkey, deriv_path) def to_string(self) -> str: + """ + Serialize the pubkey expression to a string to be used in a descriptor + + :return: The pubkey expression as a string + """ s = "" if self.origin: s += "[{}]".format(self.origin.to_string()) @@ -158,17 +207,31 @@ def __lt__(self, other: 'PubkeyProvider') -> bool: class Descriptor(object): + r""" + An abstract class for Descriptors themselves. + Descriptors can contain multiple :class:`PubkeyProvider`\ s and no more than one ``Descriptor`` as a subdescriptor. + """ def __init__( self, pubkeys: List['PubkeyProvider'], subdescriptor: Optional['Descriptor'], name: str ) -> None: + r""" + :param pubkeys: The :class:`PubkeyProvider`\ s that are part of this descriptor + :param subdescriptor: The ``Descriptor`` that is part of this descriptor + :param name: The name of the function for this descriptor + """ self.pubkeys = pubkeys self.subdescriptor = subdescriptor self.name = name def to_string_no_checksum(self) -> str: + """ + Serializes the descriptor as a string without the descriptor checksum + + :return: The descriptor string + """ return "{}({}{})".format( self.name, ",".join([p.to_string() for p in self.pubkeys]), @@ -176,6 +239,11 @@ def to_string_no_checksum(self) -> str: ) def to_string(self) -> str: + """ + Serializes the descriptor as a string wtih the checksum + + :return: The descriptor with a checksum + """ return AddChecksum(self.to_string_no_checksum()) def expand(self, pos: int) -> "ExpandedScripts": @@ -186,10 +254,16 @@ def expand(self, pos: int) -> "ExpandedScripts": class PKHDescriptor(Descriptor): + """ + A descriptor for ``pkh()`` descriptors + """ def __init__( self, pubkey: 'PubkeyProvider' ) -> None: + """ + :param pubkey: The :class:`PubkeyProvider` for this descriptor + """ super().__init__([pubkey], None, "pkh") def expand(self, pos: int) -> "ExpandedScripts": @@ -198,10 +272,16 @@ def expand(self, pos: int) -> "ExpandedScripts": class WPKHDescriptor(Descriptor): + """ + A descriptor for ``wpkh()`` descriptors + """ def __init__( self, pubkey: 'PubkeyProvider' ) -> None: + """ + :param pubkey: The :class:`PubkeyProvider` for this descriptor + """ super().__init__([pubkey], None, "wpkh") def expand(self, pos: int) -> "ExpandedScripts": @@ -210,12 +290,20 @@ def expand(self, pos: int) -> "ExpandedScripts": class MultisigDescriptor(Descriptor): + """ + A descriptor for ``multi()`` and ``sortedmulti()`` descriptors + """ def __init__( self, pubkeys: List['PubkeyProvider'], thresh: int, is_sorted: bool ) -> None: + r""" + :param pubkeys: The :class:`PubkeyProvider`\ s for this descriptor + :param thresh: The number of keys required to sign this multisig + :param is_sorted: Whether this is a ``sortedmulti()`` descriptor + """ super().__init__(pubkeys, None, "sortedmulti" if is_sorted else "multi") self.thresh = thresh if is_sorted: @@ -240,10 +328,16 @@ def expand(self, pos: int) -> "ExpandedScripts": class SHDescriptor(Descriptor): + """ + A descriptor for ``sh()`` descriptors + """ def __init__( self, subdescriptor: Optional['Descriptor'] ) -> None: + """ + :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor + """ super().__init__([], subdescriptor, "sh") def expand(self, pos: int) -> "ExpandedScripts": @@ -254,10 +348,16 @@ def expand(self, pos: int) -> "ExpandedScripts": class WSHDescriptor(Descriptor): + """ + A descriptor for ``wsh()`` descriptors + """ def __init__( self, subdescriptor: Optional['Descriptor'] ) -> None: + """ + :param pubkey: The :class:`Descriptor` that is a sub-descriptor for this descriptor + """ super().__init__([], subdescriptor, "wsh") def expand(self, pos: int) -> "ExpandedScripts": @@ -270,6 +370,10 @@ def expand(self, pos: int) -> "ExpandedScripts": def _get_func_expr(s: str) -> Tuple[str, str]: """ Get the function name and then the expression inside + + :param s: The string that begins with a function name + :return: The function name as the first element of the tuple, and the expression contained within the function as the second element + :raises: ValueError: if a matching pair of parentheses cannot be found """ start = s.index("(") end = s.rindex(")") @@ -277,6 +381,12 @@ def _get_func_expr(s: str) -> Tuple[str, str]: def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: + """ + Parses an individual pubkey expression from a string that may contain more than one pubkey expression. + + :param expr: The expression to parse a pubkey expression from + :return: The :class:`PubkeyProvider` that is parsed as the first item of a tuple, and the remainder of the expression as the second item. + """ end = len(expr) comma_idx = expr.find(",") next_expr = "" @@ -287,12 +397,35 @@ def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: class _ParseDescriptorContext(Enum): + """ + :meta private: + + Enum representing the level that we are in when parsing a descriptor. + Some expressions aren't allowed at certain levels, this helps us track those. + """ + TOP = 1 + """The top level, not within any descriptor""" + P2SH = 2 + """Within a ``sh()`` descriptor""" + P2WSH = 3 + """Within a ``wsh()`` descriptor""" def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor': + """ + :meta private: + + Parse a descriptor given the context level we are in. + Used recursively to parse subdescriptors + + :param desc: The descriptor string to parse + :param ctx: The :class:`_ParseDescriptorContext` indicating the level we are in + :return: The parsed descriptor + :raises: ValueError: if the descriptor is malformed + """ func, expr = _get_func_expr(desc) if func == "pkh": pubkey, expr = parse_pubkey(expr) @@ -342,6 +475,14 @@ def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor' def parse_descriptor(desc: str) -> 'Descriptor': + """ + Parse a descriptor string into a :class:`Descriptor`. + Validates the checksum if one is provided in the string + + :param desc: The descriptor string + :return: The parsed :class:`Descriptor` + :raises: ValueError: if the descriptor string is malformed + """ i = desc.find("#") if i != -1: checksum = desc[i + 1:] From da818c58c87db8e3a1be52009be12fd147b85037 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 22:31:32 -0400 Subject: [PATCH 342/634] Docstring base58.py --- hwilib/_base58.py | 50 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/hwilib/_base58.py b/hwilib/_base58.py index 9df34be14..e036908af 100644 --- a/hwilib/_base58.py +++ b/hwilib/_base58.py @@ -1,3 +1,7 @@ +""" +Base 58 conversion utilities +**************************** +""" # # base58.py @@ -19,7 +23,12 @@ def encode(b: bytes) -> str: - """Encode bytes to a base58-encoded string""" + """ + Encode bytes to a base58-encoded string + + :param b: Bytes to encode + :return: Base58 encoded string of ``b`` + """ # Convert big-endian bytes to integer n: int = int('0x0' + hexlify(b).decode('utf8'), 16) @@ -42,7 +51,12 @@ def encode(b: bytes) -> str: return b58_digits[0] * pad + res def decode(s: str) -> bytes: - """Decode a base58-encoding string, returning bytes""" + """ + Decode a base58-encoding string, returning bytes + + :param s: Base48 string to decode + :return: Bytes encoded by ``s`` + """ if not s: return b'' @@ -71,27 +85,59 @@ def decode(s: str) -> bytes: return b'\x00' * pad + res def get_xpub_fingerprint(s: str) -> bytes: + """ + Get the parent fingerprint from an extended public key + + :param s: The extended pubkey + :return: The parent fingerprint bytes + """ data = decode(s) fingerprint = data[5:9] return fingerprint def get_xpub_fingerprint_hex(xpub: str) -> str: + """ + Get the parent fingerprint as a hex string from an extended public key + + :param s: The extended pubkey + :return: The parent fingerprint as a hex string + """ data = decode(xpub) fingerprint = data[5:9] return hexlify(fingerprint).decode() def to_address(b: bytes, version: bytes) -> str: + """ + Base58 Check Encode the data with the version number. + Used to encode legacy style addresses. + + :param b: The data to encode + :param version: The version number to encode with + :return: The Base58 Check Encoded string + """ data = version + b checksum = hash256(data)[0:4] data += checksum return encode(data) def xpub_to_pub_hex(xpub: str) -> str: + """ + Get the public key as a string from the extended public key. + + :param xpub: The extended pubkey + :return: The pubkey hex string + """ data = decode(xpub) pubkey = data[-37:-4] return hexlify(pubkey).decode() def xpub_main_2_test(xpub: str) -> str: + """ + Convert an extended pubkey from mainnet version to testnet version. + + :param xpub: The extended pubkey + :return: The extended pubkey re-encoded using testnet version bytes + """ data = decode(xpub) test_data = b'\x04\x35\x87\xCF' + data[4:-4] checksum = hash256(test_data)[0:4] From e47b8ed65f1963c5543bd11dc16545a91a5c9fb4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 27 Aug 2020 23:00:52 -0400 Subject: [PATCH 343/634] Configure readthedocs.org --- .readthedocs.yml | 5 +++++ docs/conf.py | 13 +++++++++---- docs/requirements.txt | 2 ++ poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..c1543b649 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,5 @@ +version: 2 + +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index a25aa1429..8ae7deaa6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,9 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys + +sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- @@ -32,6 +33,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. @@ -52,7 +54,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -67,3 +69,6 @@ # Show both class and init docstring autoclass_content = "both" + +# Mock these imports +autodoc_mock_imports = ["hid", "ecdsa", "pyaes", "mnemonic", "typing_extensions", "usb1", "PySide2"] diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..e289e8bd7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinxcontrib-autoprogram>=0.1.5 +sphinx>=3.2.1 diff --git a/poetry.lock b/poetry.lock index 3562e5b3c..118237166 100644 --- a/poetry.lock +++ b/poetry.lock @@ -513,6 +513,20 @@ docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] +[[package]] +name = "sphinx-rtd-theme" +version = "0.5.1" +description = "Read the Docs theme for Sphinx" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +sphinx = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" @@ -639,7 +653,7 @@ qt = ["pyside2"] [metadata] lock-version = "1.1" python-versions = "^3.6,<3.10" -content-hash = "ece80fa838f45c4e0f26b2def64ede901603792dcbeaaca889dfbce0c41aae21" +content-hash = "da6bc3b9d0b1eb4346d7d97532d88e3c30610b6d1f3a95eda1a3971b28e73bf1" [metadata.files] alabaster = [ @@ -980,6 +994,10 @@ sphinx = [ {file = "Sphinx-3.5.1-py3-none-any.whl", hash = "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c"}, {file = "Sphinx-3.5.1.tar.gz", hash = "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9"}, ] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"}, + {file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"}, +] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, diff --git a/pyproject.toml b/pyproject.toml index e417543aa..3f7190c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ autopep8 = "^1.4" flake8 = "^3.7" mypy = "^0.790" sphinx = "^3.2.1" +sphinx-rtd-theme = "^0.5.1" [tool.poetry.scripts] hwi = 'hwilib._cli:main' From 7e089313d79ef21cddaefeb50dba0e51ab5cebf1 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 28 Aug 2020 01:18:28 -0400 Subject: [PATCH 344/634] Make RTD theme wider --- docs/_templates/layout.html | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/_templates/layout.html diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 000000000..cf2b2cd07 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,8 @@ +{% extends "!layout.html" %} + {% block footer %} {{ super() }} + + + +{% endblock %} From 1d2c21fb36eced8f8772826f69f36d687d03b5e9 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 26 Aug 2020 18:04:42 -0400 Subject: [PATCH 345/634] Add docs/usage/cli-usage.rst Some changes to hwilib/cli.py is needed to automatically populate the docs with the usage text --- docs/conf.py | 1 + docs/usage/cli-usage.rst | 11 +++++++++++ docs/usage/index.rst | 1 + hwilib/_cli.py | 13 +++++++++---- poetry.lock | 18 +++++++++++++++++- pyproject.toml | 1 + 6 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 docs/usage/cli-usage.rst diff --git a/docs/conf.py b/docs/conf.py index 8ae7deaa6..5956eda79 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ extensions = [ "sphinx.ext.autodoc", "sphinx_rtd_theme", + "sphinxcontrib.autoprogram", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/usage/cli-usage.rst b/docs/usage/cli-usage.rst new file mode 100644 index 000000000..9d2a71c06 --- /dev/null +++ b/docs/usage/cli-usage.rst @@ -0,0 +1,11 @@ +Command Line Usage +****************** + +HWI is primarily used from the command line. +Users can use the ``hwi`` command directly, or the HWI self-contained binaries can be distributed with third party software and executed by the software. + +The usage of ``hwi`` can be found with ``hwi --help``. + +.. autoprogram:: hwilib._cli:get_parser() + :prog: hwi + :groups: diff --git a/docs/usage/index.rst b/docs/usage/index.rst index 957cda375..85a3f6caa 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -5,4 +5,5 @@ Usage :maxdepth: 2 installation + cli-usage api-usage diff --git a/hwilib/_cli.py b/hwilib/_cli.py index 2073d8b6e..fc6ba3060 100644 --- a/hwilib/_cli.py +++ b/hwilib/_cli.py @@ -131,7 +131,7 @@ def error(self, message: str) -> NoReturn: print(json.dumps(error)) self.exit(2) -def process_commands(cli_args: List[str]) -> Any: +def get_parser() -> HWIArgumentParser: parser = HWIArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__)) parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to') parser.add_argument('--device-type', '-t', help='Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.') @@ -175,9 +175,9 @@ def process_commands(cli_args: List[str]) -> Any: getkeypool_parser.add_argument('--internal', action='store_true', help='Indicates that the keys are change keys') kp_type_group = getkeypool_parser.add_mutually_exclusive_group() kp_type_group.add_argument("--addr-type", help="The address type (and default derivation path) to produce descriptors for", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) # type: ignore - kp_type_group.add_argument('--all', action='store_true', help='Generate addresses for all standard address types (default paths: m/{44,49,84}h/0h/0h/[0,1]/*)') + kp_type_group.add_argument('--all', action='store_true', help='Generate addresses for all standard address types (default paths: ``m/{44,49,84}h/0h/0h/[0,1]/*)``') getkeypool_parser.add_argument('--account', help='BIP43 account', type=int, default=0) - getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. m/84h/0h/0h/1/* with --addr-type wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') + getkeypool_parser.add_argument('--path', help='Derivation path, default follows BIP43 convention, e.g. ``m/84h/0h/0h/1/*`` with --addr-type wpkh --internal. If this argument and --internal is not given, both internal and external keypools will be returned.') getkeypool_parser.add_argument('start', type=int, help='The index to start at.') getkeypool_parser.add_argument('end', type=int, help='The index to end at.') getkeypool_parser.set_defaults(func=getkeypool_handler) @@ -189,7 +189,7 @@ def process_commands(cli_args: List[str]) -> Any: displayaddr_parser = subparsers.add_parser('displayaddress', help='Display an address') group = displayaddr_parser.add_mutually_exclusive_group(required=True) group.add_argument('--desc', help='Output Descriptor. E.g. wpkh([00000000/84h/0h/0h]xpub.../0/0), where 00000000 must match --fingerprint and xpub can be obtained with getxpub. See doc/descriptors.md in Bitcoin Core') - group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. m/84h/0h/0h/1/*') + group.add_argument('--path', help='The BIP 32 derivation path of the key embedded in the address, default follows BIP43 convention, e.g. ``m/84h/0h/0h/1/*``') displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.PKH) # type: ignore displayaddr_parser.set_defaults(func=displayaddress_handler) @@ -226,6 +226,11 @@ def process_commands(cli_args: List[str]) -> Any: udevrules_parser.add_argument('--location', help='The path where the udev rules files will be copied', default='/etc/udev/rules.d/') udevrules_parser.set_defaults(func=install_udev_rules_handler) + return parser + +def process_commands(cli_args: List[str]) -> Any: + parser = get_parser() + if any(arg == '--stdin' for arg in cli_args): while True: try: diff --git a/poetry.lock b/poetry.lock index 118237166..b8f5e9a01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -539,6 +539,18 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "sphinxcontrib-autoprogram" +version = "0.1.7" +description = "Documenting CLI programs" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" +Sphinx = ">=1.2" + [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" @@ -653,7 +665,7 @@ qt = ["pyside2"] [metadata] lock-version = "1.1" python-versions = "^3.6,<3.10" -content-hash = "da6bc3b9d0b1eb4346d7d97532d88e3c30610b6d1f3a95eda1a3971b28e73bf1" +content-hash = "c0072453b031fbfb5de06909f5bc9a7263c2e1241b8f2e9c7a5ad4c9b4ddbc2a" [metadata.files] alabaster = [ @@ -1002,6 +1014,10 @@ sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] +sphinxcontrib-autoprogram = [ + {file = "sphinxcontrib-autoprogram-0.1.7.tar.gz", hash = "sha256:bc642e3f2817a7539f306e021697f72b225bea5ad23b30dc14a7b9d1408d1f1a"}, + {file = "sphinxcontrib_autoprogram-0.1.7-py2.py3-none-any.whl", hash = "sha256:746adb4214c3d2917af948499b3ed4b7b88b208a48c96368c0cff356474dba42"}, +] sphinxcontrib-devhelp = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, diff --git a/pyproject.toml b/pyproject.toml index 3f7190c0b..7ab4f8817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ flake8 = "^3.7" mypy = "^0.790" sphinx = "^3.2.1" sphinx-rtd-theme = "^0.5.1" +sphinxcontrib-autoprogram = "^0.1.6" [tool.poetry.scripts] hwi = 'hwilib._cli:main' From ea692e6187fafa256276403745a13a03e55cecbc Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 19:55:40 -0500 Subject: [PATCH 346/634] Docstring hwilib/bech32.py --- hwilib/_bech32.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hwilib/_bech32.py b/hwilib/_bech32.py index 500f766f2..24c5b7698 100644 --- a/hwilib/_bech32.py +++ b/hwilib/_bech32.py @@ -18,7 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -"""Reference implementation for Bech32 and segwit addresses.""" +""" +Bech32 Conversion Utilities +*************************** + +Reference implementation for Bech32 and segwit addresses. +""" from typing import ( List, From 06b7ec324b19081b6e50e1781da20e266ec18acd Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 19:55:53 -0500 Subject: [PATCH 347/634] Docstring hwilib/common.py --- hwilib/common.py | 51 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/hwilib/common.py b/hwilib/common.py index 1da2a57a5..c02da6f4e 100644 --- a/hwilib/common.py +++ b/hwilib/common.py @@ -1,3 +1,8 @@ +""" +Common Classes and Utilities +**************************** +""" + import hashlib from enum import Enum @@ -6,10 +11,13 @@ class Chain(Enum): - MAIN = 0 - TEST = 1 - REGTEST = 2 - SIGNET = 3 + """ + The blockchain network to use + """ + MAIN = 0 #: Bitcoin Main network + TEST = 1 #: Bitcoin Test network + REGTEST = 2 #: Bitcoin Core Regression Test network + SIGNET = 3 #: Bitcoin Signet def __str__(self) -> str: return self.name.lower() @@ -26,9 +34,12 @@ def argparse(s: str) -> Union['Chain', str]: class AddressType(Enum): - PKH = 1 - WPKH = 2 - SH_WPKH = 3 + """ + The type of address to use + """ + PKH = 1 #: Legacy address type. P2PKH for single sig, P2SH for scripts. + WPKH = 2 #: Native segwit address type. P2WPKH for single sig, P2WPSH for scripts. + SH_WPKH = 3 #: Nested segwit address type. P2SH-P2WPKH for single sig, P2SH-P2WPSH for scripts. def __str__(self) -> str: return self.name.lower() @@ -45,16 +56,42 @@ def argparse(s: str) -> Union['AddressType', str]: def sha256(s: bytes) -> bytes: + """ + Perform a single SHA256 hash. + + :param s: Bytes to hash + :return: The hash + """ return hashlib.new('sha256', s).digest() def ripemd160(s: bytes) -> bytes: + """ + Perform a single RIPEMD160 hash. + + :param s: Bytes to hash + :return: The hash + """ return hashlib.new('ripemd160', s).digest() def hash256(s: bytes) -> bytes: + """ + Perform a double SHA256 hash. + A SHA256 is performed on the input, and then a second + SHA256 is performed on the result of the first SHA256 + + :param s: Bytes to hash + :return: The hash + """ return sha256(sha256(s)) def hash160(s: bytes) -> bytes: + """ + perform a single SHA256 hash followed by a single RIPEMD160 hash on the result of the SHA256 hash. + + :param s: Bytes to hash + :return: The hash + """ return ripemd160(sha256(s)) From eff54058564c52e96d5986995d007a120d44bd30 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 17 Feb 2021 19:56:01 -0500 Subject: [PATCH 348/634] Docstring hwilib/key.py --- hwilib/key.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/hwilib/key.py b/hwilib/key.py index 73c7fbddf..e412980af 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -3,6 +3,13 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Key Classes and Utilities +************************* + +Classes and utilities for working with extended public keys, key origins, and other key related things. +""" + from . import _base58 as base58 from .common import ( hash256, @@ -93,6 +100,9 @@ def point_to_bytes(p: Point) -> bytes: # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): + """ + A BIP 32 extended public key. + """ MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' @@ -100,6 +110,15 @@ class ExtendedKey(object): TESTNET_PRIVATE = b'\x04\x35\x83\x94' def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: bytes) -> None: + """ + :param version: The version bytes for this xpub + :param depth: The depth of this xpub as defined in BIP 32 + :param parent_fingerprint: The 4 byte fingerprint of the parent xpub as defined in BIP 32 + :param child_num: The number of this xpub as defined in BIP 32 + :param chaincode: The chaincode of this xpub as defined in BIP 32 + :param privkey: The private key for this xpub if available + :param pubkey: The public key for this xpub + """ self.version: bytes = version self.is_testnet: bool = version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE self.is_private: bool = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE @@ -112,6 +131,11 @@ def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_ @classmethod def deserialize(cls, xpub: str) -> 'ExtendedKey': + """ + Create an :class:`~ExtendedKey` from a Base58 check encoded xpub + + :param xpub: The Base58 check encoded xpub + """ data = base58.decode(xpub)[:-4] # Decoded xpub without checksum version = data[0:4] @@ -132,6 +156,12 @@ def deserialize(cls, xpub: str) -> 'ExtendedKey': return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey) def serialize(self) -> bytes: + """ + Serialize the ExtendedKey with the serialization format described in BIP 32. + Does not create an xpub string, but the bytes serialized here can be Base58 check encoded into one. + + :return: BIP 32 serialized extended key + """ r = self.version + struct.pack('B', self.depth) + self.parent_fingerprint + struct.pack('>I', self.child_num) + self.chaincode if self.is_private: if self.privkey is None: @@ -142,11 +172,21 @@ def serialize(self) -> bytes: return r def to_string(self) -> str: + """ + Serialize the ExtendedKey as a Base58 check encoded xpub string + + :return: Base58 check encoded xpub + """ data = self.serialize() checksum = hash256(data)[0:4] return base58.encode(data + checksum) def get_printable_dict(self) -> Dict[str, object]: + """ + Get the attributes of this ExtendedKey as a dictionary that can be printed + + :return: Dictionary containing ExtendedKey information that can be printed + """ d: Dict[str, object] = {} d['testnet'] = self.is_testnet d['private'] = self.is_private @@ -160,6 +200,11 @@ def get_printable_dict(self) -> Dict[str, object]: return d def derive_pub(self, i: int) -> 'ExtendedKey': + """ + Derive the public key at the given child index. + + :param i: The child index of the pubkey to derive + """ if is_hardened(i): raise ValueError("Index cannot be larger than 2^31") @@ -182,6 +227,11 @@ def derive_pub(self, i: int) -> 'ExtendedKey': return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth + 1, fingerprint, i, chaincode, None, pubkey) def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey': + """ + Derive the public key at the given path + + :param path: Sequence of integers for the path of the pubkey to derive + """ key = self for i in path: key = key.derive_pub(i) @@ -189,7 +239,14 @@ def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey': class KeyOriginInfo(object): + """ + Object representing the origin of a key. + """ def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: + """ + :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from + :param path: The derivation path to reach this key from the key at ``fingerprint`` + """ self.fingerprint: bytes = fingerprint self.path: Sequence[int] = path From 0dd535db61c99d1ae1a0eaa1d7738a4b28fa0fda Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Feb 2021 18:06:16 -0500 Subject: [PATCH 349/634] Docstring hwilib/psbt.py --- hwilib/psbt.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/hwilib/psbt.py b/hwilib/psbt.py index 24acbaada..cf6aa1ac0 100644 --- a/hwilib/psbt.py +++ b/hwilib/psbt.py @@ -1,3 +1,8 @@ +""" +PSBT Classes and Utilities +************************** +""" + import base64 import struct @@ -31,6 +36,16 @@ def DeserializeHDKeypath( hd_keypaths: MutableMapping[bytes, KeyOriginInfo], expected_sizes: Sequence[int], ) -> None: + """ + :meta private: + + Deserialize a serialized PSBT public key and keypath key-value pair. + + :param f: The byte stream to read the value from. + :param key: The bytes of the key of the key-value pair. + :param hd_keypaths: Dictionary of public key bytes to their :class:`~hwilib.key.KeyOriginInfo`. + :param expected_sizes: List of key lengths expected for the keypair being deserialized. + """ if len(key) not in expected_sizes: raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey. Length: {}".format(len(key))) pubkey = key[1:] @@ -40,6 +55,15 @@ def DeserializeHDKeypath( hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) -> bytes: + """ + :meta private: + + Serialize a public key to :class:`~hwilib.key.KeyOriginInfo` mapping as a PSBT key-value pair. + + :param hd_keypaths: The mapping of public key to keypath + :param type: The PSBT type bytes to use + :returns: The serialized keypaths + """ r = b"" for pubkey, path in sorted(hd_keypaths.items()): r += ser_string(type + pubkey) @@ -48,6 +72,9 @@ def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) return r class PartiallySignedInput: + """ + An object for a PSBT input map. + """ def __init__(self) -> None: self.non_witness_utxo: Optional[CTransaction] = None self.witness_utxo: Optional[CTxOut] = None @@ -61,6 +88,9 @@ def __init__(self) -> None: self.unknown: Dict[bytes, bytes] = {} def set_null(self) -> None: + """ + Clear all values in this PSBT input map. + """ self.non_witness_utxo = None self.witness_utxo = None self.partial_sigs.clear() @@ -73,6 +103,11 @@ def set_null(self) -> None: self.unknown.clear() def deserialize(self, f: Readable) -> None: + """ + Deserialize a serialized PSBT input. + + :param f: A byte stream containing the serialized PSBT input + """ while True: # read the key try: @@ -163,6 +198,11 @@ def deserialize(self, f: Readable) -> None: self.unknown[key] = unknown_bytes def serialize(self) -> bytes: + """ + Serialize this PSBT input + + :returns: The serialized PSBT input + """ r = b"" if self.non_witness_utxo: @@ -212,6 +252,9 @@ def serialize(self) -> bytes: return r class PartiallySignedOutput: + """ + An object for a PSBT output map. + """ def __init__(self) -> None: self.redeem_script = b"" self.witness_script = b"" @@ -219,12 +262,20 @@ def __init__(self) -> None: self.unknown: Dict[bytes, bytes] = {} def set_null(self) -> None: + """ + Clear this PSBT output map + """ self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths.clear() self.unknown.clear() def deserialize(self, f: Readable) -> None: + """ + Deserialize a serialized PSBT output map + + :param f: A byte stream containing the serialized PSBT output + """ while True: # read the key try: @@ -263,6 +314,11 @@ def deserialize(self, f: Readable) -> None: self.unknown[key] = value def serialize(self) -> bytes: + """ + Serialize this PSBT output + + :returns: The serialized PSBT output + """ r = b"" if len(self.redeem_script) != 0: r += ser_string(b"\x00") @@ -283,8 +339,14 @@ def serialize(self) -> bytes: return r class PSBT(object): + """ + A class representing a PSBT + """ def __init__(self, tx: Optional[CTransaction] = None) -> None: + """ + :param tx: A Bitcoin transaction that specifies the inputs and outputs to use + """ if tx: self.tx = tx else: @@ -295,6 +357,11 @@ def __init__(self, tx: Optional[CTransaction] = None) -> None: self.xpub: Dict[bytes, KeyOriginInfo] = {} def deserialize(self, psbt: str) -> None: + """ + Deserialize a base 64 encoded PSBT. + + :param psbt: A base 64 PSBT. + """ psbt_bytes = base64.b64decode(psbt.strip()) f = BufferedReader(BytesIO(psbt_bytes)) # type: ignore end = len(psbt_bytes) @@ -375,6 +442,11 @@ def deserialize(self, psbt: str) -> None: raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") def serialize(self) -> str: + """ + Serialize the PSBT as a base 64 encoded string. + + :returns: The base 64 encoded string. + """ r = b"" # magic bytes From 12f0319d0c10f6318d42c5594ce07fbd1c48a5b3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Feb 2021 18:14:48 -0500 Subject: [PATCH 350/634] Docstring hwilib/_script.py --- hwilib/_script.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/hwilib/_script.py b/hwilib/_script.py index 6f30ce758..870ed653b 100644 --- a/hwilib/_script.py +++ b/hwilib/_script.py @@ -1,3 +1,8 @@ +""" +Bitcoin Script utilities +************************ +""" + from typing import ( Optional, Sequence, @@ -6,22 +11,55 @@ def is_opreturn(script: bytes) -> bool: + """ + Determine whether a script is an OP_RETURN output script. + + :param script: The script + :returns: Whether the script is an OP_RETURN output script + """ return script[0] == 0x6a def is_p2sh(script: bytes) -> bool: + """ + Determine whether a script is a P2SH output script. + + :param script: The script + :returns: Whether the script is a P2SH output script + """ return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 def is_p2pkh(script: bytes) -> bool: + """ + Determine whether a script is a P2PKH output script. + + :param script: The script + :returns: Whether the script is a P2PKH output script + """ return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac def is_p2pk(script: bytes) -> bool: + """ + Determine whether a script is a P2PK output script. + + :param script: The script + :returns: Whether the script is a P2PK output script + """ return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac def is_witness(script: bytes) -> Tuple[bool, int, bytes]: + """ + Determine whether a script is a segwit output script. + If so, also returns the witness version and witness program. + + :param script: The script + :returns: A tuple of a bool indicating whether the script is a segwit output script, + an int representing the witness version, + and the bytes of the witness program. + """ if len(script) < 4 or len(script) > 42: return (False, 0, b"") @@ -35,6 +73,12 @@ def is_witness(script: bytes) -> Tuple[bool, int, bytes]: def is_p2wpkh(script: bytes) -> bool: + """ + Determine whether a script is a P2WPKH output script. + + :param script: The script + :returns: Whether the script is a P2WPKH output script + """ is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False @@ -44,6 +88,12 @@ def is_p2wpkh(script: bytes) -> bool: def is_p2wsh(script: bytes) -> bool: + """ + Determine whether a script is a P2WSH output script. + + :param script: The script + :returns: Whether the script is a P2WSH output script + """ is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False @@ -55,6 +105,14 @@ def is_p2wsh(script: bytes) -> bool: # Only handles up to 15 of 15. Returns None if this script is not a # multisig script. Returns (m, pubkeys) otherwise. def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: + """ + Determine whether a script is a multisig script. If so, determine the parameters of that multisig. + + :param script: The script + :returns: ``None`` if the script is not multisig. + If multisig, returns a tuple of the number of signers required, + and a sequence of public key bytes. + """ # Get m m = script[0] - 80 if m < 1 or m > 15: From f82bc839dee28e22ac802663799fdbb4bc297225 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Feb 2021 18:37:28 -0500 Subject: [PATCH 351/634] Docstring hwilib/_serialize.py --- docs/development/index.rst | 1 + hwilib/_serialize.py | 87 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/docs/development/index.rst b/docs/development/index.rst index 99931a498..43c58b57b 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -5,3 +5,4 @@ Development :caption: Contents: release-process + internal-api diff --git a/hwilib/_serialize.py b/hwilib/_serialize.py index c2f005162..7640e5d38 100644 --- a/hwilib/_serialize.py +++ b/hwilib/_serialize.py @@ -4,12 +4,12 @@ # Copyright (c) 2010-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Bitcoin Object Python Serializations +""" +Bitcoin Object Python Serializations +************************************ Modified from the test/test_framework/mininode.py file from the Bitcoin repository - -ser_*, deser_*: functions that handle serialization/deserialization """ import struct @@ -37,6 +37,12 @@ def serialize(self) -> bytes: # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: + """ + Serialize an integer using Bitcoin's compact size unsigned integer serialization. + + :param size: The int to serialize + :returns: The int serialized as a compact size unsigned integer + """ r = b"" if size < 253: r = struct.pack("B", size) @@ -49,6 +55,12 @@ def ser_compact_size(size: int) -> bytes: return r def deser_compact_size(f: Readable) -> int: + """ + Deserialize a compact size unsigned integer from the beginning of the byte stream. + + :param f: The byte stream + :returns: The integer that was serialized + """ nit: int = struct.unpack(" int: return nit def deser_string(f: Readable) -> bytes: + """ + Deserialize a variable length byte string serialized with Bitcoin's variable length string serialization from a byte stream. + + :param f: The byte stream + :returns: The byte string that was serialized + """ nit = deser_compact_size(f) return f.read(nit) def ser_string(s: bytes) -> bytes: + """ + Serialize a byte string with Bitcoin's variable length string serialization. + + :param s: The byte string to be serialized + :returns: The serialized byte string + """ return ser_compact_size(len(s)) + s def deser_uint256(f: Readable) -> int: + """ + Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte stream. + + :param f: The byte stream. + :returns: The integer that was serialized + """ r = 0 for i in range(8): t = struct.unpack(" int: def ser_uint256(u: int) -> bytes: + """ + Serialize a 256 bit integer with Bitcoin's 256 bit integer serialization. + + :param u: The integer to serialize + :returns: The serialized 256 bit integer + """ rs = b"" for _ in range(8): rs += struct.pack(" bytes: def uint256_from_str(s: bytes) -> int: + """ + Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte string. + + :param s: The byte string + :returns: The integer that was serialized + """ r = 0 t = struct.unpack(" int: D = TypeVar("D", bound=Deserializable) def deser_vector(f: Readable, c: Callable[[], D]) -> List[D]: + """ + Deserialize a vector of objects with Bitcoin's object vector serialization from a byte stream. + + :param f: The byte stream + :param c: The class of object to deserialize for each object in the vector + :returns: A list of objects that were serialized + """ nit = deser_compact_size(f) r = [] for _ in range(nit): @@ -101,6 +150,12 @@ def deser_vector(f: Readable, c: Callable[[], D]) -> List[D]: def ser_vector(v: Sequence[Serializable]) -> bytes: + """ + Serialize a vector of objects with Bitcoin's object vector serialzation. + + :param v: The list of objects to serialize + :returns: The serialized objects + """ r = ser_compact_size(len(v)) for i in v: r += i.serialize() @@ -108,6 +163,12 @@ def ser_vector(v: Sequence[Serializable]) -> bytes: def deser_string_vector(f: Readable) -> List[bytes]: + """ + Deserialize a vector of byte strings from a byte stream. + + :param f: The byte stream + :returns: The list of byte strings that were serialized + """ nit = deser_compact_size(f) r = [] for _ in range(nit): @@ -117,12 +178,25 @@ def deser_string_vector(f: Readable) -> List[bytes]: def ser_string_vector(v: List[bytes]) -> bytes: + """ + Serialize a list of byte strings as a vector of byte strings. + + :param v: The list of byte strings to serialize + :returns: The serialized list of byte strings + """ r = ser_compact_size(len(v)) for sv in v: r += ser_string(sv) return r def ser_sig_der(r: bytes, s: bytes) -> bytes: + """ + Serialize the ``r`` and ``s`` values of an ECDSA signature using DER. + + :param r: The ``r`` value bytes + :param s: The ``s`` value bytes + :returns: The DER encoded signature + """ sig = b"\x30" # Make r and s as short as possible @@ -167,6 +241,13 @@ def ser_sig_der(r: bytes, s: bytes) -> bytes: return sig def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: + """ + Serialize the ``r`` and ``s`` values of an ECDSA signature using the compact signature serialization scheme. + + :param r: The ``r`` value bytes + :param s: The ``s`` value bytes + :returns: The compact signature + """ rec = struct.unpack("B", recid)[0] prefix = struct.pack("B", 27 + 4 + rec) From 4b5792a1de0144ccac85e5115f1e5c4b0e1d1c48 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 23 Feb 2021 18:44:00 -0500 Subject: [PATCH 352/634] Add internal api to sphinx docs --- docs/development/internal-api.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/development/internal-api.rst diff --git a/docs/development/internal-api.rst b/docs/development/internal-api.rst new file mode 100644 index 000000000..3de8092ed --- /dev/null +++ b/docs/development/internal-api.rst @@ -0,0 +1,13 @@ +Internal API Documentation +========================== + +In addition to the public API, the classes and functions documented here are available for use within HWI itself. + +.. automodule:: hwilib._base58 + :members: +.. automodule:: hwilib._bech32 + :members: +.. automodule:: hwilib._script + :members: +.. automodule:: hwilib._serialize + :members: From 6e58e9990b63933e41445bad1a0f9bdcf7be645a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 24 Feb 2021 14:35:34 -0500 Subject: [PATCH 353/634] Update Bitcoin Core usage doc to use descriptor wallets --- docs/examples/bitcoin-core-usage.rst | 76 +++++++++++++--------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/docs/examples/bitcoin-core-usage.rst b/docs/examples/bitcoin-core-usage.rst index 87d2fa002..234956a02 100644 --- a/docs/examples/bitcoin-core-usage.rst +++ b/docs/examples/bitcoin-core-usage.rst @@ -1,7 +1,7 @@ Using Bitcoin Core with Hardware Wallets **************************************** -This approach is fairly manual, requires the command line, and Bitcoin Core >=0.18.0. +This approach is fairly manual, requires the command line, and Bitcoin Core >=0.21.0. Note: For this guide, code lines prefixed with ``$`` means that the command is typed in the terminal. Lines without ``$`` are output of the commands. @@ -18,7 +18,8 @@ Bitcoin Core This method of using hardware wallets uses Bitcoin Core as the wallet for monitoring the blockchain. It allows a user to use their own full node instead of relying on an SPV wallet or vendor provided software. -HWI works with Bitcoin Core as of commit `c576979b78b541bf3b4a7cbeee989b55d268e3e1 `_ It is usable with Bitcoin Core >=0.18.0. +HWI works with Bitcoin Core >=0.18.0. +However this guide will require Bitcoin Core >=0.21.0 as it uses Descriptor Wallets. Setup ===== @@ -43,28 +44,27 @@ You may need some dependencies, on ubuntu install ``libudev-dev`` and ``libusb-1 Now we need to find our hardware wallet. We do this using:: $ ./hwi.py enumerate - [{"fingerprint": "8038ecd9", "serial_number": "205A32753042", "type": "coldcard", "path": "0001:0005:00"}] + [{"type": "coldcard", "model": "coldcard", "path": "0003:0005:00", "needs_pin_sent": false, "needs_passphrase_sent": false, "fingerprint": "e5dbc9cb"}] -For this example, we will use the Coldcard. As we can see, the device path is ``0001:0005:00``. The fingerprint of the master key is ``8038ecd9``. Now that we have the device, we can issue commands to it. So now we want to get some keys and import them into Core. +For this example, we will use the Coldcard. As we can see, the device path is ``0003:0005:00``. The fingerprint of the master key is ``e5dbc9cb``. Now that we have the device, we can issue commands to it. So now we want to get some keys and import them into Core. We will be fetching keys at the BIP 84 default. If ``--path`` and ``--internal`` are not specified, both receiving and change address descriptors are generated. :: - $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 0 1000 - [{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}] + $ ./hwi.py -f e5dbc9cb getkeypool --addr-type wpkh 0 1000 + [{"desc": "wpkh([e5dbc9cb/84'/0'/0']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/0/*)#cwyap6p3", "range": [0, 1000], "timestamp": "now", "internal": false, "keypool": true, "active": true, "watchonly": true}, {"desc": "wpkh([e5dbc9cb/84'/0'/0']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/1/*)#f6puu03f", "range": [0, 1000], "timestamp": "now", "internal": true, "keypool": true, "active": true, "watchonly": true}] -We now create a new Bitcoin Core wallet and import the keys into Bitcoin Core. The output is formatted properly for Bitcoin Core so it can be copy and pasted. +We now create a new Bitcoin Core Descriptor Wallet and import the keys into Bitcoin Core. The output is formatted properly for Bitcoin Core so it can be copy and pasted. :: - $ ../bitcoin/src/bitcoin-cli createwallet "coldcard" true + $ ../bitcoin/src/bitcoin-cli -named createwallet wallet_name=hwicoldcard disable_private_keys=true descriptors=true { - "name": "coldcard", - "warning": "" + "name": "hwicoldcard", + "warning": "Wallet is an experimental descriptor wallet" } - $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}, {"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#nl2rc26w", "internal": true, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}]' - + $ ../bitcoin/src/bitcoin-cli -rpcwallet=hwicoldcard importdescriptors '[{"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/0/*)#cwyap6p3", "range": [0, 1000], "timestamp": "now", "internal": false, "keypool": true, "active": true, "watchonly": true}, {"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/1/*)#f6puu03f", "range": [0, 1000], "timestamp": "now", "internal": true, "keypool": true, "active": true, "watchonly": true}]' [ { "success": true @@ -76,7 +76,7 @@ We now create a new Bitcoin Core wallet and import the keys into Bitcoin Core. T The Bitcoin Core wallet is now setup to watch two thousand keys (1000 normal, 1000 change) from your hardware wallet and you can use it to track your balances and create transactions. The transactions will need to be signed through HWI. -If the wallet was previously used, you will need to rescan the blockchain. You can either do this using the ``rescanblockchain`` command or editing the ``timestamp`` in the ``importmulti`` command. +If the wallet was previously used, you will need to rescan the blockchain. You can either do this using the ``rescanblockchain`` command or editing the ``timestamp`` in the ``importdescriptors`` command. Here are some examples (```` refers to a block height before the wallet was created). :: @@ -84,8 +84,8 @@ Here are some examples (```` refers to a block height before the wa $ ../bitcoin/src/bitcoin-cli rescanblockchain $ ../bitcoin/src/bitcoin-cli rescanblockchain 500000 # Rescan from block 500000 - $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": , "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' - $ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": 500000, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' # Imports and rescans from block 500000 + $ ../bitcoin/src/bitcoin-cli -rpcwallet=hwicoldcard importdescriptors '[{"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/0/*)#cwyap6p3", "range": [0, 1000], "timestamp": , "internal": false, "keypool": true, "active": true, "watchonly": true}, {"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/1/*)#f6puu03f", "range": [0, 1000], "timestamp": , "internal": true, "keypool": true, "active": true, "watchonly": true}]' + $ ../bitcoin/src/bitcoin-cli -rpcwallet=hwicoldcard importdescriptors '[{"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/0/*)#cwyap6p3", "range": [0, 1000], "timestamp": 500000, "internal": false, "keypool": true, "active": true, "watchonly": true}, {"desc": "wpkh([e5dbc9cb/84\'/0\'/0\']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/1/*)#f6puu03f", "range": [0, 1000], "timestamp": 500000, "internal": true, "keypool": true, "active": true, "watchonly": true}]' # Imports and rescans from block 500000 Usage ===== @@ -105,39 +105,35 @@ To get a new address, use ``getnewaddress`` as you normally would :: - $ src/bitcoin-cli -rpcwallet=coldcard getnewaddress - bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s + $ src/bitcoin-cli -rpcwallet=hwicoldcard getnewaddress + bc1q2xsn08w749d2tfm7qrkvztlxfmq2564sly4dwl This address belongs to your hardware wallet. You can check this by doing ``getaddressinfo``:: - $ src/bitcoin-cli -rpcwallet=coldcard getaddressinfo bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s + $ src/bitcoin-cli -rpcwallet=hwicoldcard getaddressinfo bc1q2xsn08w749d2tfm7qrkvztlxfmq2564sly4dwl { - "address": "bcrt1qu8qe24zq5e2ahh4nkl6g5ysxlpn3nyf0xt026s", - "scriptPubKey": "0014e1c1955440a655dbdeb3b7f48a1206f86719912f", - "ismine": false, - "iswatchonly": true, + "address": "bc1q2xsn08w749d2tfm7qrkvztlxfmq2564sly4dwl", + "scriptPubKey": "001451a1379ddea95aa5a77e00ecc12fe64ec0aa6ab0", + "ismine": true, "solvable": true, + "desc": "wpkh([e5dbc9cb/84'/0'/0'/0/0]0325ccb1f60a3d0640cbc3bfa1cefc34512d50c32d0e7c102b62e18f23ab69fbc5)#je3ch2kg", + "parent_desc": "wpkh([e5dbc9cb/84'/0'/0']xpub6CbtS57jivMSuzcvp5YZxp6JhUU8YWup2axi2xkQRVHY8w4otp8YkEvfWBHgE5rA2AJYNHquuRoLFFdWeSi1UgVohcUeM7SkE9c8NftRwRJ/0/*)#cwyap6p3", + "iswatchonly": false, "isscript": false, "iswitness": true, "witness_version": 0, - "witness_program": "e1c1955440a655dbdeb3b7f48a1206f86719912f", - "pubkey": "022320f1cf72e7ba2cef6be32d7493ce3bd4c6a2575fe51ce260377adc165603d4", - "label": "", + "witness_program": "51a1379ddea95aa5a77e00ecc12fe64ec0aa6ab0", + "pubkey": "0325ccb1f60a3d0640cbc3bfa1cefc34512d50c32d0e7c102b62e18f23ab69fbc5", "ischange": false, - "timestamp": 1541688305, - "hdkeypath": "m/84'/1'/0'/0/0", + "timestamp": 1614190663, + "hdkeypath": "m/84'/0'/0'/0/0", "hdseedid": "0000000000000000000000000000000000000000", - "hdmasterkeyid": "00000000000000000000000000000000d9ec3880", + "hdmasterfingerprint": "e5dbc9cb", "labels": [ - { - "name": "", - "purpose": "receive" - } + "" ] } -Notice how the pubkey is the one that was specified as the very first thing being imported to your wallet. - You can give this out to people as you normally would. When coins are sent to it, you will see them in your Bitcoin Core wallet as watch-only. Sending @@ -148,7 +144,7 @@ This PSBT can be used with HWI to produce a signed PSBT which can then be finali For example, suppose I am sending to 1 BTC to bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy. First I create a funded psbt with BIP 32 derivation paths to be included:: - $ src/bitcoin-cli -rpcwallet=coldcard walletcreatefundedpsbt '[]' '[{"bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy":1}]' 0 '{"includeWatching":true}' true + $ src/bitcoin-cli -rpcwallet=hwicoldcard walletcreatefundedpsbt '[]' '[{"bc1q257z5t76hedc36wmmzva05890ny3kxd7xfwrgy":1}]' 0 '{"includeWatching":true}' true { "psbt": "cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA", "fee": 0.00002820, @@ -251,7 +247,7 @@ Once the transaction has been inspected and everything looks good, the transacti :: $ cd ../HWI - $ ./hwi.py -f 8038ecd9 --testnet signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA + $ ./hwi.py -f e5dbc9cb --testnet signtx cHNidP8BAHECAAAAAU8KWkCU7H4MYBiZHmLey6FavV3L3xLfy4tVEZoubx+2AAAAAAD+////AgDh9QUAAAAAFgAUVTwqL9q+W4jp29iZ19DlfMkbGb78eNcXAAAAABYAFLHuX3WRuPs3ypeQOziNw5qFlBH8AAAAAAABAR8AZc0dAAAAABYAFOHBlVRAplXb3rO39IoSBvhnGZEvIgYCIyDxz3Lnuizva+MtdJPOO9TGoldf5RziYDd63BZWA9QYgDjs2VQAAIABAACAAAAAgAAAAAAAAAAAAAAiAgP0HMQ2K693zCXTCudBUzemDhxLmFGETOnAV7vgDz2r9RiAOOzZVAAAgAEAAIAAAACAAQAAAAAAAAAA Follow the onscreen instructions, check everything, and approve the transaction. The result will look like:: @@ -273,12 +269,8 @@ We can now take the PSBT, finalize it, and broadcast it with Bitcoin Core Refilling the keypools ---------------------- -When the keypools run out, they can be refilled by using the ``getkeypool`` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following ``getkeypool`` commands:: - - $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh 1000 2000 - $ ./hwi.py -f 8038ecd9 getkeypool --addr-type wpkh --internal 1000 2000 - -The output can be imported with ``importmulti`` as shown in the Setup steps. +Descriptor wallets will constantly generate new addresses from the imported descriptors. +It is not necessary to import additional keys or descriptors to refresh the keypool, Bitcoin Core will do so automatically by using the descriptors. Derivation Path BIP Compliance ============================== From ec547380b0d3f6e9b5bc71a8d6bebd3a904046fa Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 25 Feb 2021 15:36:05 -0500 Subject: [PATCH 354/634] Mention readthedocs.io --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 42d59618e..e3bd80841 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Bitcoin Hardware Wallet Interface [![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI) +[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest) The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets. It provides a standard way for software to work with hardware wallets without needing to implement device specific drivers. @@ -84,13 +85,17 @@ pass the `--help` parameter after the command name; for example: ./hwi.py getdescriptors --help ``` -## Device Support +## Documentation -For documentation on devices supported and how they are supported, please check the [devicesupport page](docs/devices/index.rst) +Documentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/). -## Using with Bitcoin Core +### Device Support -See [Using Bitcoin Core with Hardware Wallets](docs/examples/bitcoin-core-usage.rst). +For documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix) + +### Using with Bitcoin Core + +See [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html). ## License From 6361a18ace6f1b4d39da9620c5f68ee922c37144 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Thu, 25 Feb 2021 23:53:29 -0500 Subject: [PATCH 355/634] Specify a device support policy --- docs/devices/index.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/devices/index.rst b/docs/devices/index.rst index 7286d9cf9..8c42396f9 100644 --- a/docs/devices/index.rst +++ b/docs/devices/index.rst @@ -56,6 +56,34 @@ The table below lists what devices and features are supported for each device. | Display on device screen | ✓ | ✓ | ✓ | ✓ | ― | ✓ | ✓ | ✓ | +------------------------------------+---------------+---------------+------------+----------------+----------+----------+---------+----------+ +Support Policy +================ + +For a device to be supported by HWI, it must: + +* Use open source firmware as much as possible + + * Entirely closed source devices will be rejected + * Devices may have closed source firmware components if required to under a NDA (e.g. a secure element with NDA) + +* Publicly documented communication protocol + + * It is preferred to both document the protocol and provide a Python library for using it + * The library, with its own documentation, can suffice as "publicly documented" + +* Either (but preferably both): + + * A simulator/emulator is available for automated tests to be run + * A promise to maintain and support from the vendor: + +Device support may be dropped: + +* If promised vendor maintenance and support disappears + + * If there are continuous issues with the device and the vendor has failed to provide support and updates + +* If the device no longer receives security devices and there are known vulnerabilities and issues + Device APIs =========== From 7de0237c5590efde7364209c30fc3dbb77544786 Mon Sep 17 00:00:00 2001 From: 8go Date: Tue, 12 Jan 2021 19:36:09 +0000 Subject: [PATCH 356/634] detailed walk-through of using Bitcoin Core with a hardware wallet --- docs/walk-through/README.rst | 191 ++++++++++++++++++ .../Screenshot01_HWI_Empty-State.png | Bin 0 -> 27394 bytes .../Screenshot02_HWI_HWW-Selected.png | Bin 0 -> 55051 bytes ...eenshot03-Core-Initial-Wallet-Overview.png | Bin 0 -> 96418 bytes ...reenshot04_HWI_Address-Display-Request.png | Bin 0 -> 15694 bytes ...reenshot05_HWW_Display-Receive-Address.png | Bin 0 -> 292046 bytes ...eenshot06_HWI_Address-Display-Response.png | Bin 0 -> 21420 bytes ...reenshot07_Core_Console-getaddressinfo.png | Bin 0 -> 109216 bytes ...reenshot08_HWI_Address-Display-Request.png | Bin 0 -> 15977 bytes ...creenshot09_HWW_Display-Change-Address.png | Bin 0 -> 301176 bytes ...eenshot10_HWI_Address-Display-Response.png | Bin 0 -> 21229 bytes ...reenshot11_Core_Console-getaddressinfo.png | Bin 0 -> 78396 bytes .../Screenshot12_Core_Send-Tab.png | Bin 0 -> 126922 bytes .../Screenshot13_Core_Create-Unsigned-Tx.png | Bin 0 -> 65406 bytes ...eenshot14_Core_Paste-PSBT-to-Clipboard.png | Bin 0 -> 17102 bytes .../Screenshot15_HWI_Empty-PSBT.png | Bin 0 -> 16377 bytes .../Screenshot16_HWI_Prepare-PSBT-signing.png | Bin 0 -> 42206 bytes ...nshot17_HWW_Confirm-Amount-Destination.png | Bin 0 -> 302441 bytes .../Screenshot18_HWW_Confirm-Locktime.png | Bin 0 -> 304935 bytes .../Screenshot19_HWW_Confirm-Amount-Fees.png | Bin 0 -> 301774 bytes ...creenshot20_Core_Console_getblockcount.png | Bin 0 -> 24884 bytes .../Screenshot21_HWI_Show-Signed-PSBT.png | Bin 0 -> 71278 bytes ...creenshot22_Core_Broadcast-Signed-PSBT.png | Bin 0 -> 50495 bytes .../Screenshot23_Core_Transactions-Tab.png | Bin 0 -> 93399 bytes .../Screenshot24_Core_Transaction-Details.png | Bin 0 -> 64263 bytes ...reenshot25_Core_Console-gettransaction.png | Bin 0 -> 117183 bytes 26 files changed, 191 insertions(+) create mode 100644 docs/walk-through/README.rst create mode 100644 docs/walk-through/Screenshot01_HWI_Empty-State.png create mode 100644 docs/walk-through/Screenshot02_HWI_HWW-Selected.png create mode 100644 docs/walk-through/Screenshot03-Core-Initial-Wallet-Overview.png create mode 100644 docs/walk-through/Screenshot04_HWI_Address-Display-Request.png create mode 100644 docs/walk-through/Screenshot05_HWW_Display-Receive-Address.png create mode 100644 docs/walk-through/Screenshot06_HWI_Address-Display-Response.png create mode 100644 docs/walk-through/Screenshot07_Core_Console-getaddressinfo.png create mode 100644 docs/walk-through/Screenshot08_HWI_Address-Display-Request.png create mode 100644 docs/walk-through/Screenshot09_HWW_Display-Change-Address.png create mode 100644 docs/walk-through/Screenshot10_HWI_Address-Display-Response.png create mode 100644 docs/walk-through/Screenshot11_Core_Console-getaddressinfo.png create mode 100644 docs/walk-through/Screenshot12_Core_Send-Tab.png create mode 100644 docs/walk-through/Screenshot13_Core_Create-Unsigned-Tx.png create mode 100644 docs/walk-through/Screenshot14_Core_Paste-PSBT-to-Clipboard.png create mode 100644 docs/walk-through/Screenshot15_HWI_Empty-PSBT.png create mode 100644 docs/walk-through/Screenshot16_HWI_Prepare-PSBT-signing.png create mode 100644 docs/walk-through/Screenshot17_HWW_Confirm-Amount-Destination.png create mode 100644 docs/walk-through/Screenshot18_HWW_Confirm-Locktime.png create mode 100644 docs/walk-through/Screenshot19_HWW_Confirm-Amount-Fees.png create mode 100644 docs/walk-through/Screenshot20_Core_Console_getblockcount.png create mode 100644 docs/walk-through/Screenshot21_HWI_Show-Signed-PSBT.png create mode 100644 docs/walk-through/Screenshot22_Core_Broadcast-Signed-PSBT.png create mode 100644 docs/walk-through/Screenshot23_Core_Transactions-Tab.png create mode 100644 docs/walk-through/Screenshot24_Core_Transaction-Details.png create mode 100644 docs/walk-through/Screenshot25_Core_Console-gettransaction.png diff --git a/docs/walk-through/README.rst b/docs/walk-through/README.rst new file mode 100644 index 000000000..e141cb2e7 --- /dev/null +++ b/docs/walk-through/README.rst @@ -0,0 +1,191 @@ +How to Use a Hardware Wallet with Bitcoin Core Wallet +***************************************************** + +A Step-by-step Walk-Through +*************************** + +Summary: On this page we describe step-by-step and show in screenshots how to use a hardware wallet and HWI +together with a Bitcoin Core Wallet. As hardware wallet example we have used a Trezor. + +Create a watch-only Bitcoin Core wallet for Trezor +================================================== + +Create your watch-only Bitcoin Core Wallet as described in `Using Bitcoin Core with Hardware Wallets <../bitcoin-core-usage.rst>`_. +You find all the details well described in this link. But in summary, one opens a terminal and runs ``bitcoind``. E.g. + +:: + + bitcoind -testnet -datadir=$HOME/.bitcoin-testnet + +for a testnet ``bitcoind`` daemon, or + +:: + + bitcoind + +for a mainnet, i.e. regular, ``bitcoind`` daemon. + +Then in another terminal run commands similar to these, adapted to your environment: + +:: + + hwi.py enumerate # this shows you the fingerprint of your hardware wallet + FINGERPRINT_TESTNET="yourHardwareWalletFingerprint" # shown by "hwi enumerate" + # in this example we use SEGWIT BECH32 ADDRESSES + DERIVATIONPATH_TESTNET=1 # testnet uses derivation paths like m/84h/1h/0h/0/* and m/84h/1h/0h/1/* + DERIVATIONPATH_MAINNET=0 # mainnet uses derivation paths like m/84h/0h/0h/0/* and m/84h/0h/0h/1/* + # if the mainnet path is used on testnet, it will work too, but Trezor device gives warnings + # of unknown address on Trezor display. This is not recommended. Use the correct derivation path + # for the corresponding network! + wallet=wallet.test + rec=$(hwi --testnet -f $FINGERPRINT_TESTNET getkeypool --addr-type wpkh --path m/84h/${DERIVATIONPATH_TESTNET}h/0h/0/* --keypool 0 1000) + chg=$(hwi --testnet -f $FINGERPRINT_TESTNET getkeypool --addr-type wpkh --path m/84h/${DERIVATIONPATH_TESTNET}h/0h/1/* --keypool --internal 0 1000) + bitcoin-cli -testnet createwallet "$wallet" true + bitcoin-cli -testnet -rpcwallet="$wallet" importmulti "$rec" + bitcoin-cli -testnet -rpcwallet="$wallet" importmulti "$chg" + echo "If the hardware wallet has been used before and holds funds then you should rescan. Rescanning might take 30 minutes." + bitcoin-cli -testnet -rpcwallet="$wallet" rescanblockchain # full rescan + # after rescan unload wallet + bitcoin-cli -testnet -rpcwallet="$wallet" unloadwallet + +This script needs to be adapted to your needs. If you are creating a wallet for mainnet get rid of ``-testnet`` and ``--testnet`` and +use ``DERIVATIONPATH_MAINNET`` instead of ``DERIVATIONPATH_TESTNET``. Adapt the derivation paths to your needs. +Now that the watch-only Bitcoin Core wallet has been created, stop ``bitcoind`` with control-C. We are ready to use the wallet. + +Send funds with Bitcoin Core and Trezor using HWI +================================================= + +* our example does everything on the Bitcoin testnet, so watch out, your addresses and paths will differ +* TREZOR: plug in your hardware wallet, e.g. your Trezor, put in the PIN if any +* HWI: type ``hwi-qt.py --testnet`` to start HWI GUI for testnet (type ``hwi-qt.py`` to start HWI GUI for mainnet) + +.. image:: Screenshot01_HWI_Empty-State.png + +* TREZOR: your hardware wallet, e.g. Trezor, might prompt you for a passphrase, enter passphrase on hardware wallet (if any) +* HWI: select your hardware wallet in HWI GUI + +.. image:: Screenshot02_HWI_HWW-Selected.png + +* CORE: start Bitcoin Core wallet, e.g. ``bitcoin-qt -testnet`` (or ``bitcoin-qt`` for mainnet) + +.. image:: Screenshot03-Core-Initial-Wallet-Overview.png + +* on the very first run it might be a good idea to verify that the wallet has been created correctly +* on first run **verify your wallet** (optional) +* HWI: HWI GUI -> "Display Address", since we use BECH32 address, select "P2WPKH", + enter "m/84h/1h/0h/0/0" (testnet derivation path) (or "m/84h/0h/0h/0/0" on mainnet). + This path represents the first receiving address. Click "Go". + In our example, it shows address "tb1q0r2gn9wzfjm5j5zshx5yp5342h928c8pmllfep". + +.. image:: Screenshot04_HWI_Address-Display-Request.png + +.. image:: Screenshot05_HWW_Display-Receive-Address.png + +.. image:: Screenshot06_HWI_Address-Display-Response.png + +* CORE: In Core Wallet, open "Console", enter ``getaddressinfo tb1q0r2gn9wzfjm5j5zshx5yp5342h928c8pmllfep``, + observe these values: + It is crucial that ``solvable`` shows as ``true``! + + * "solvable": true, + * "iswatchonly": true, + * "hdkeypath": "m/84'/1'/0'/0/0", + +.. image:: Screenshot07_Core_Console-getaddressinfo.png + +* HWI: In HWI GUI main window click "Display Address", since we use BECH32 address, + select "P2WPKH", enter "m/84h/1h/0h/1/0" (testnet derivation path) (or "m/84h/0h/0h/1/0" on mainnet). + This path represents the first change address. Click "Go". + In our example it shows address "tb1qca3u0ka22c934jfqw7gjr9vg4gwwjldpzatrh5". + +.. image:: Screenshot08_HWI_Address-Display-Request.png + +.. image:: Screenshot09_HWW_Display-Change-Address.png + +.. image:: Screenshot10_HWI_Address-Display-Response.png + +* CORE: In Core Wallet, open "Console", enter ``getaddressinfo tb1qca3u0ka22c934jfqw7gjr9vg4gwwjldpzatrh5``, + observe these values: + It is crucial that ``solvable`` shows as ``true``! + + * "solvable": true, + * "iswatchonly": true, + * "hdkeypath": "m/84'/1'/0'/1/0", + +.. image:: Screenshot11_Core_Console-getaddressinfo.png + +* If you see the same addresses for the same paths on Trezor, in HWI and in Bitcoin Core Wallet + you can rest assured that the wallet has been created correctly and + that the Bitcoin Core wallet corresponds to your Trezor device. + +* Now let us **send funds**. + +* CORE: To send funds, open the "Send" tab in Bitcoin Core Wallet, + then select input, amount, fees, etc. Once satisifed, click "Create Unsigned", + verify any displayed information, then click "Create Unsigned" again. + The PSBT (Partially Signed Bitcoin Transaction) is now on the clipboard. + +.. image:: Screenshot12_Core_Send-Tab.png + +.. image:: Screenshot13_Core_Create-Unsigned-Tx.png + +.. image:: Screenshot14_Core_Paste-PSBT-to-Clipboard.png + +* HWI: In HWI GUI main window click "Sign PSBT", then paste PSBT from clipboard + into the above text field. After paste, click "Sign PSBT". + +.. image:: Screenshot15_HWI_Empty-PSBT.png + +.. image:: Screenshot16_HWI_Prepare-PSBT-signing.png + +* TREZOR: verify signing on Trezor, accept operation on Trezor if all is correct + +.. image:: Screenshot17_HWW_Confirm-Amount-Destination.png + +.. image:: Screenshot18_HWW_Confirm-Locktime.png + +.. image:: Screenshot19_HWW_Confirm-Amount-Fees.png + +* CORE: Trezor prints blockheight of locktime which can optionally be verified in + Bitcoin Core Wallet (Console -> ``getblockcount``). For a simple send the locktime + is now and you should get the current blockheight. + +.. image:: Screenshot20_Core_Console_getblockcount.png + +* HWI: upon accepting on Trezor, the HWI bottom text area is filled. + Select the bottom output, and copy full output from the bottom text area to the clipboard + +.. image:: Screenshot21_HWI_Show-Signed-PSBT.png + +* CORE: In Bitcoin Core Wallet, go to the pull-down menu: select File -> Load from Clipboard. + +.. image:: Screenshot22_Core_Broadcast-Signed-PSBT.png + +* CORE: In Core Wallet, visually verify again, then click "Broadcast Tx" button. + Once broadcasted, click "Close". + The funds have been sent to the mempool awaiting confirmations on the Bitcoin network. + +* CORE: In Core Wallet, go to "Transactions" tab. Here you can find the just + sent transaction in the top line. Wait for confirmations. + +.. image:: Screenshot23_Core_Transactions-Tab.png + +* CORE: Optionally double click transaction to see transaction details. + +.. image:: Screenshot24_Core_Transaction-Details.png + +* CORE: Optionally, one can also see the transaction details in the + Console -> ``gettransaction 58d9dccd190250742c47733f3c0f0d33075d65621196434f163f92b69847843f`` + +.. image:: Screenshot25_Core_Console-gettransaction.png + +* HWI: close HWI GUI +* CORE: close Core wallet +* you are done! Pad yourself on the shoulder ;) + +Versions Used +============= + +* This walk-trough was done in Janary 2021 +* HWI version 1.2.1 +* Bitcoin 0.21.0 diff --git a/docs/walk-through/Screenshot01_HWI_Empty-State.png b/docs/walk-through/Screenshot01_HWI_Empty-State.png new file mode 100644 index 0000000000000000000000000000000000000000..e6dbaaf4c4267c53d60b2a322a149e0bf99d97a0 GIT binary patch literal 27394 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot02_HWI_HWW-Selected.png b/docs/walk-through/Screenshot02_HWI_HWW-Selected.png new file mode 100644 index 0000000000000000000000000000000000000000..c94fec2f08432d41ac3c1196202de1d25575d15b GIT binary patch literal 55051 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 b>Z_9P(LEPthaQ}(;AJ^}s@cV{i#Poj&qsr0 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot03-Core-Initial-Wallet-Overview.png b/docs/walk-through/Screenshot03-Core-Initial-Wallet-Overview.png new file mode 100644 index 0000000000000000000000000000000000000000..37de9f77b7cec7ed48d41a4d35909fa3f619e265 GIT binary patch literal 96418 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 I>zopr01g%-;s5{u literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot04_HWI_Address-Display-Request.png b/docs/walk-through/Screenshot04_HWI_Address-Display-Request.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ae4d85612365a56755b90eb1230ab37b7d35b6 GIT binary patch literal 15694 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 OlF#Ix=8L{~^M3$hb`_og literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot05_HWW_Display-Receive-Address.png b/docs/walk-through/Screenshot05_HWW_Display-Receive-Address.png new file mode 100644 index 0000000000000000000000000000000000000000..94d6772a1b3bed6e7fd497c242d7f3431fdfb686 GIT binary patch literal 292046 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

#)rQCFkT<_GS=Cr5~0S6 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

_njr|c5(>WwWnvh$Ta$#jj`*2*6 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot10_HWI_Address-Display-Response.png b/docs/walk-through/Screenshot10_HWI_Address-Display-Response.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7e5a2d613b5a8da624f0bf62ec2f1326065965 GIT binary patch literal 21229 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot11_Core_Console-getaddressinfo.png b/docs/walk-through/Screenshot11_Core_Console-getaddressinfo.png new file mode 100644 index 0000000000000000000000000000000000000000..32c3b4cf5290734a70822e2cd576ab7d3261ed21 GIT binary patch literal 78396 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

vsEmB7qZ4^t5dVoQKUQO3dyuho_J)+t_iFD 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 Y;duDIY#Yh>JBnD$uI+{Vo=4vA?;e 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 z z*(W@@`ya{iFJdSLI#9K%R^_)|W{72q%-ZQ09xp9_9DY*i(waNV`S!Q^luOSeQSJ9f zZV{i#Q6FcKdS*TE`jKZu7Wa4o3;$A&*InU>+91b~7caMVD~nwCH}1$K zAZTFHJ5*&jI)nz8QwHED^f2+|erbKKL zI@~i*RmUkaIGxlsL{~DF)oXUdOuEWGk2@=Zs$U7j$`GG@c=_6kz^QE#Yu>?ncW!q!lMs7x7sgX&*+Ws z*yBhwQphA(rKT2_lqh;Lh0Ua#*S>&D!fB)94io78m;ou7r?0eK z%H(-mTpl~ST|kZL-MRa}K5oqs6`T(jC?OMCHOm>k47#7;W1$sBW4m-D-AQdD{i%?a ziwfDV+(xh_&iBMwvGR{N905)YP!moNg=ivU_`QCVwszB<+gVeE9CMxfmH=5Uo0h3|wb#wa$D=RB>so!u3 z$ICDDb_@>VW5DUCy=i~z7P}>TM~Ae{>YzVUBr_6e5^hEolgS2mxOx1Yr)D25MZ&g! z4x0L_lb@T@q=_)Y6>LdvoPMg#+dqFaYWEwLaT_Wm0Z9sXr$XUtHkH8qc?Ova-o9Si z$VwX5Y6VV`KPBF|Pt$n`Sd{UGFO*qWSWed#PwoX&yN7rkx+SZHXMjSG-Y5+@c~dA- z(rBnkq?fhe^L+a7(q~tp8zv?cXJMy!4G!9twh5R;8hdSSUuJ*V^Budv-+!f8w!-2R8|!T*SPE1fedD7)F7YW=N;P*q8!!IZ}A9DgZsNoGFD>#H2TzQ$8 zV!=}nNCYL@!^`9K?lh<5*zp3k=j7%4T%w6+X!<~kgAtCaDC;wt)0Zz_W~+#!f+%RJ z?M}`;h(nK7?9cocmiC{_?B^>=S>{pfn(z`XmX{TC4RzWh)$SHi$5GGGrPqvM+q_9! z$WhY8Lr}|`7#IX5B(RJImoNB1`?b7WYjj?-HUDY(*eP-1wDS+_b2(^wLEhLIck1N$ zOd0~B8R>pHvfn}@0@J2zO;gd2vbHsd$%l+$k)!%d#FJzuWV0o1=%^^5D0Nr|rTa*f zkG(;f>-&aO*w=+v*;qF8Hzq&Odu_=aj=3apt*!2~(XTR+(d)B%PpfPkX2aNCSVZ`$ za#VQg9XT?-JJdqG-zJXze2}QH<=DZWvtJ^f! z<4EKcN_Hc*?n}lZ@M}}tQ!?+Ln85T`2QaariqLI^Hbc3wN;}GlSh5VLM<$ zrJc?-(N;0cWNVgF%jo$BldNIptgG4DIm%2xVk<1PY(b+*rmXKs4P}IVj4*81M(LQC z1VRrB5>JSuGpR!ZtB$yrv!ow9ke&G*X5B-*)0Nrxp1nsxXdc?}QN;9`NxmK@0hOMyi4%bL8<6+O~aVONigpy3T&@U(}2z%SgiQP$)M2kPZ85=_@tU{QGwmZurP3-n{Xw zl(N%mnjeNFqSZS?c;kEt;3bpwg3Jnl$WexBF31f&J z4$gC@W%iro0)KiRipy1l8_BVLM7i&^W{&dtQ;3ujZ6jOi707UyjU5EID%B)%B6wN{n=ecuCohokNaNKg!(us{jB1 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot14_Core_Paste-PSBT-to-Clipboard.png b/docs/walk-through/Screenshot14_Core_Paste-PSBT-to-Clipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..6a658868031a81a87191c9b2b94f24c792123a34 GIT binary patch literal 17102 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 Xo%Ml{It~0l1%!l%tZ=#D*YE!gD9f)M literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot15_HWI_Empty-PSBT.png b/docs/walk-through/Screenshot15_HWI_Empty-PSBT.png new file mode 100644 index 0000000000000000000000000000000000000000..3fc20ab65b4df63bf97e65072fb6a37526c25b1f GIT binary patch literal 16377 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot16_HWI_Prepare-PSBT-signing.png b/docs/walk-through/Screenshot16_HWI_Prepare-PSBT-signing.png new file mode 100644 index 0000000000000000000000000000000000000000..4681077066afe9fa8ef65ee483bf37916ba24f6a GIT binary patch literal 42206 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

$rf`AE?7+=e(a%9^`B|5h0)7>(@g;#6@3L9^ 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 Mp00i_>zopr0Hi^4#6Gn$z$*P_g3An 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot08_HWI_Address-Display-Request.png b/docs/walk-through/Screenshot08_HWI_Address-Display-Request.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a5dc8919a3414184c335aca596de5744f760f5 GIT binary patch literal 15977 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 literal 0 HcmV?d00001 diff --git a/docs/walk-through/Screenshot09_HWW_Display-Change-Address.png b/docs/walk-through/Screenshot09_HWW_Display-Change-Address.png new file mode 100644 index 0000000000000000000000000000000000000000..1e645ad6af170691b6ea2d9687a9f717091e659f GIT binary patch literal 301176 zcmV)2K+M01P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*tlAE~^ME~OyJ_1nKkApSBH}LWO66}^V^4K0n zxG9lr@>_tatgK9+-TBAgulpN6snz6CuC4S^JU^+YZi6??|NJ>$ubG`Ye%{%By>T61pPRQojHvZs{@0)O z^Z&iO-FrT>8>tY6@|o1<7Vi5p95{(me5Vy2mH(Od^*uU|=7(P)`R$jVJXYYwA^ICq z*e;B4!wH+$6&7PmcC7Mhwk*ESTCCP>MIS>fJ2AAE(nl{I@PN!&X#m=H;UFCpeog0E&ZgcPlj zpuxy3CYxj+nr54rPsIjJ+YpJ!i+UsbsrIuT1wYApU=++}8nS1HAx8D00+;b#1 z^5E>j5o1i7ai*DPnRT|==UBjJrIlA%b+y&k*tX+NJMXgVZoBVs!U0O|q?1oM^|aH^ zxM1zZn{K}4*4u8s<44xQSK0pl_#d(szOrU7Q#!8x$Qqy4stdhK1o2E(&d8XhK*p;w z0H94d^DU%g%A9iMyHOR{7-Uv%cJRZ6LJHFfmT&lxyPui+%XxFe`oHDP{z~SYQulwz zoVgQ!%iEu1ZHo!l%h;z16;mInzAMgYBgW?L$WZ3aVJ9XTR-!~!D1$g~i+NiPkW4QY zHzA9ouiEY$^~SLXb7Eekr4M&%PU)BGpJ)s3socYzM|%y@e)HHVbpzMwJgGp6Iy zz#*jSlBAP!--*iiliA6O{2G@%!8z}D3)_6gx%JqS9b<<8gah6 z4`p=2hwi~hzwh^vHG&imoti@<6<(=83gXmJK>&*CTD&K}-zHb4kO^B(t`wp*!_8HQ zHHJBbF%HscYh|Tp*tF2DzADE5S(#f4xkW}nfm2Y9FkE;geuCaVAx9t-p&~{q@DuY0 zgTXboZjD7KIqwbwr9h2G2a`c8(N=()=Sq`eB{FmNvBoS%-}=%5+;w`b%TR}gk^#l5 z0)|^dhQGQa73Bn=GFz{ZC+yVE-F|TIVjJS#mItb!lw`gne>jae&H?E1*i>is(UR*) zL__`FL3O~Fy-kRfq%jecS|x02*Hre2;s7{ef}XQ#Cn1zo%QOe!h)MTx_A*9BesTnG zbOU&Tj}BFIMhSaD4db5kDz6<{$ec*3HH+^6O$~B3pLWbO#kq<=Hi_YcXL!>~YzvO; z)Cs{pHYkb&`k7p%;Ugd>JiVE-%fK;jAi$sw^gH%Yv!D`bJBxUvGQo~t*-l=oaC3?B z-v8NnVD1$Br{9Jd@~VcT_0^{WzRt8k6@Ii%a-ibNrK`R#A9wicSv7>DP%36h?)9XM7c;zb=2$jbGaH1tseZ;4E8M~ zL)+y8Fgt>Ou2v|LP%q7GcJ5|}R4#aY6VJslX^L5j+lYpo_onKtw?YT3upW36nqP;! z6oX*HtcqDlfIjkKdk(w!ArCSqPAgNta|H2M-s z;~VSA0Gub%)eu^wcc}#-uVdI}j=Xb4&CEgB0~7v%-Z`VuQ%+{`ZM3DVQ5)+DQ5ZZX+pJC>WkPgq`cY$ zB#=K%iF)q-Y-Bvxpt*7zaym8uL$^fArR}o-lCno(&b3vAJ(P868DZ5@7Jxi`t89^% z=rU|wC!*uh`XCPAg*E~8983_LYO%9pl^_s=mP4B zkX+c{W^qEmtK%%N4~R{=^sJ|3812FmAm2k%j-r-^grW8jw*XrlHbsmpfvaURjz?rL zb=(9pcj48Q-xL=_2_<|+?e3zw@UAUAI!B;T*L<4LNg>G7y z;Bo283(bbYqK~c`QRak#S<_m;eW61Tj)6vPlEp+E6GE|}f(#rrat(jtR!W&dN~#PU zDUP6|#q|LYsV{IW|27)EVDFA0gW6*|7eS5Ja8XqX=xCkbDS@byYKZW})owwQ_MIJxmA1z?W0y1r#HW5FbhdVpMpS#b=4;Th$Y) zF&n7+BD|>bEaXj20ckxrcqIZOX<~8;A%5&zr-aLE!WV(d6N97#!9*AHqa9+oo%VC) zNvp);O66sCyhf5STwIfgLK`#(q!O|{Dj2BDmdRF^O!JY>d-i$#P{fEo#g zN})4Was{Fg6TI0tB+`di!TjN&yTPl;ibGp01QfmqJo|DZ-Z>RR@r+KRgh1sm79uZ2 z?zyOc`q#o4%~%gT3Gi~C>eLkLl@7-e36>T9j&hW^?^z3y1(h5PhM?7m8gr~DjNHgP zed^$+XBo}q{$`50gVgX2HEO7>mQ>}nE@mv#vUZZcjkGgzg6eO8CBWV$Ad6(MSpctI zYLAvwF6j(0RiR6LgB~h_2#+9NmxKX&WS4c{J;OauzXOgT(}Z~$9=Xo31<`3Z`YBfohc7h^k3)mt#Uv+PBtAsS zZ4Sl{sg9@3X9-qRol+I)H#EWmNHJhgh?bp6rN=pwyR29S&@MKm=*2zPiSa64OC3+{ zl~A{>Q{$vB!>1Daa4=mct7Q)XhpK`l5q*ikk=>f78(WJ)(8fX=5P!C6JUgkY?T+!`iX14`{h^~l<` zBx5m=Xx%$PX_Cf8%3IoNiqM|-rKx7ov>45eP2bwF*aikk0iJn)G^(=0Vl=UXyTJF;gfl%`>rTk= zj=jo%1XqcsShVaH0S8eXIKyt!xMy#0_!6JAX#-hXafQWorsw3 zT0_(taK?AL!uUm8P(G5qG)Ny6jYviv?T#{H{ul4p^aQ|kY7|?cS+r~uy>w2$j<2Q6 z3{8UiW0b&N&5~Xgp%<%BK;qU*He<*wF-4IeGL;anPwiHBCLUE31ves!kE;3A)%5-hzwh?Nk{Kx@w0~ z_0{@ogA^j?Ii1@wX@tNY8?VRl5*;W)YT59OP#U%x*7F>4!>Ly*xO{LIHb)vHIJng3 z<+r|wAZ1ac7m}>BZK8#H*HZaLd?C4r>62y)koJLQRm7!lV4jA@GSL3K#oHSyzFdXMZ-Tofj13=s~6 zXODg@x4i;wm`OQ?P*F)mRi)L!(UIES1q>*v9um=(HN*wyQ0H`P5a_u=LVG{HTu`9L z8YD#W#WC+{9gNe{C3PHErUh&DeO>tH21HAK-Zeq8dbRu#{{bGM4~1c&>$E_juf~Ps z>?t;gizsi@3FLhi^n4RC<6X1AHPG(0^rYP_Jl>9fxZRq_x$bluQl;xe?L94&QUDj9hn6Hf!t>Eu zZ49E?Os57*Zzn)K%|0?urMywz>Fp5#T6}UW`n(=B^r$BwIJJ=8wG2&8;SRF4--(|K z&{&1x844n;!jR);?ZC|5fm8|^C#SX4sNE)t+dRYu!&b&@8hAkP4W9}Tos)iv@Ig3G z4ICaDXHLp!lveC(qbq&)EcgB9-@V$`@EGaG^J^7`Ib1vkbr1;)b3D`KswEnaYQpdi zC9QuZtNpS|%IM@;w5OAE4g!`|pg$$(9u_X>?VgKykgcuDy>WS3G=tNTN<%Pa-rI79 zuYYdNeF9(p@Fsul-??{8FYVM>Pkq>^Uf7;aFP@;BrQo$rvRZRki>7UIxK4ZY zc(w9-LQH>1@h^#(Z?6&IpcUH|T}679a219EM(LrH#9R0#L5J?!wBMCLD%@?FM}<$l z_C~Yj(L5A`zI5o^nB*rC)T7*VP9zduD=wzJSk6M<5w7-`n3rDC-|(P?DM^||IUX?z zH!}_T$apa)f*QZ=-)3^OOpfhhLbzZ3B@DVB14$&&WBOAD;hA)3hX(TA>3oWYQ>E!r zXqwC0ndMiNqP0(QDE0Zt4(+eFju1rVHYI55wby+Ai zK9yg!U`IlO!!kvcs&)bikRee>qt>rbaHxhRL~0B9vyZGlj*0F4NtGob?cj`Ht&W9A zs}(wgmdeUlpC~X}G_scxeQ!lCkcsA-KKtWC`_ANr@_DOn5F68ZRG_~Hc_GK@o>F5RrmHF=%v6DWfB>qV-0st7rmC_c+|5*R5r_!n|NYz z03m?S1&HGDg|!~MUIXEPFNd07g=4580L(REYA`WGEC7It!OU>48xaA4U`;@WK|&D{ zxa>kWAOJi95aD-Hm;yjxtqC9?Dwr$q@$DPty!m?_LkvK`O!4)60Rcc20)eSvsNq=? zFEdC0W-mO?4d{Z$0zx4w2(RDg7_S%L0Rf<@c-BI!Kp=oC|E%eie^vylg6R|2$nOJD zxG#h_h8fme00an!D)RaH!V?SOp3edKAYug&z;uU*V(5jTiWLhH0aL?TH$)VdTyPN> z9N`OMif2uT2t*ZgEr=S14$Qgo=Z8c1ZWIa+$1}emB2XPL8HjMq`9O!|VTcK+$>1e00cZ7rUn%OB;XNHx$nsLT2j+Z`Z_J@87-A4rcm(EJP!$-@A_BN<;8_y}L5M&_upWnvfyWmlfHCsB z#Bx9cs){)$MDpE5@G}aa5DZnoJ@1+t1PT$y=kp0OLm)8uvtQQ-008b89~^L>@Bpsy zf~jQu5I}8Ut~;NZ7(5&z(XmN1&#!966II1p3)2@aRa{rzX+50QX}PcYOV2AC;47E~CRuK|T(dAG{WMD8=h~~lYSa@A8UWfn$Fck;`01#NQAS(H=)NqXp`JOxz4f6L4wR|D-K_K!o z3E(SFs|fOCuayrHg7Aeg1`q;whv|Tt!rkEk{xgL@5zAq!2rM`p$fFL)I00B*QWb*; z;J#ow08zj*Xa&5JrwI@X@Nk4=0ij|T) z@)Gq&&x@c66TzGg6NN~grJ)KHMTF+LlE~K+!QU%TGx!2bUog${B_eP~1_aT3SCZc) z-=#75d;XU}%H8wqgMrQ!Kz=uYz!>6&7j%jr`0NWZeU*b@mkq!KHaC^LI)Y zJS*~fc{n`svmlV43wcH=(R^Glz#VHY{(Z|kHYZfIi$76*wtQ$1F{tFv${3J$doJfY z$om>m1qOq;C`>c?xuc6LRms9c0JHoId0q^v($7`Jpvr=jU&QkuVi*%U6slQpg)hJ- zlnF7DtUw~)ef~SV15l_K)?!=;$D9+6yvyNve|$RcW?=b3gv0cLiQ&3ln9E@*7&&imNSqyY6P=o#c`!C;3^Tn9ur53&?J;Q%5}41=!2M6&V`g_&X0A&OvD&O;OtonRImaNsiA-Nag1P4VA{0LJ7Z!T{X~ z%=2Q2B9NWa@`Yg+gcsXm!6R@#SwT-`;pLH4&stchgPfJU@^fSrmjTJyH7Ki!xhBjm zUXr}j^%>0+VtL>yNQx+%m<$xL0+6gG@+B$enou)*Tvu0+fCDC2KKTq4!Jq_Wg_O5A zpP5JgwTVG(z#$zpR1I*?WLiO+iF8$B6M?9KF$dGT8~{Y2Ix_JfyG9_t@0?#4!+1u# zlUTUtyHx=~Sv@)bEWaNEk=C=zyA74W@0IU;h_Rxqq!bv#^4Uhf*Mdq0SwVIPrUvjk zgh)PnU!7RY@{(t=7;q0R)77=Pbfb!GxOuIyOrScTvk z7Z3ttQDS4{85MzOz*h#w-G$^i713R7D`SLbQW=_sO%{CYN_c-v2TY6wXBAF>L?&?e z4DbSlvIwhZX;~q3qRirT@%iy=ha)_&SZGF7vDtUzX=Cy7gk!FWVFvd_^hH=aGp@}n zD(jg*fjP5dg9wXkz_gx=U_OhVGaw2W40BG*a0nDO@}Iq|p0%>uW6V2Zw5ai{tt=xW zV1q@eWWx02Sd!1b5~wg9MF>;|p6AJDAj*^zOj7JvP2sHVvuiSh1NSqtLo|i`J5OZZ`qM9+FTFo`H>RK77FH1S_?z3gd%)F z*%h!%$*e5Oiew;sVvK<~^MECMpb1kjx~XPGGt@B564Ep6jwKU~c9-Pt`1bLQ)gwBn zL_7&2)nOM_UhLGAFEiLBJZnK<@HMl#5ksOk0WUZ#V?BU{H67AHIUqK0F-e*Xh$t&2 z%>x<8PL$QF0PHL?a5$5Pvh&CwkQc2m1Jexrh`<<`3D*pa~`gRwgkQm|8)8KA@nH~B7bSe2^{1}%sC zgs9@Ogh`fr24W`5F$T|)@EvE1B*Myvdw4e@7LyYiPK(8E2M(VT;R%^e zC(<$(LbCW<{N49+W4dE`V6KVzJnV*Mc7SVTN5^0Dd4D1zFrS%S(#mAE=E}b(Xq0C0 z_n2uN>P)g`xaY&JitsPHZC_a|7!j7l9~?F=7D=A)YpjO%lrZlw+pwQ%!ge7nKofze z|Egugb;NLN|aJ_TWx=UGq}sAQFEva2`K3*pYL$6><~5*V40p_Ub{ z0O0|bgkl1Y@Jt4u_g{F80acbFvhXz*oS7R-@jN$diolTAgXDX7ZosoxatPcfUOL#}W+gk<#ATNAoHg-820jzTS3KG0NhZ65 zr8j&T5%4ST^?WMH3n5#=KkoUy86d}HjkiC}`@SB4bBZZXcXR;oJdQ^^AsWu<0)a2l z3__xqJ~IJ$AT$UYW3d(g86AkR5G8b{E!6*h|e2B3KWTWyNS4d3QzeJYQMtc|Ea= zPmgCTshg7BkL2fuVj?gXx@b~DwFqEjg_1{Q`NI7?FerzV*=o529%O~3CJ+}~3?AU+ zQ4#1EnPDRkbK)AT{6g~~6BDh*F&NlX6pO7tBp|Sa9AYA15Xp+ZhE4Nv24)V{1ArRl zbapS^mqJ)Ust7Nn5{C}FMz+njPgq; za4y5WDhIiDreecqo@c#g9a;_gN3AEY(Uz|C)V`wI*XH9kySzW_(6u!bSA68oHhUtbo)w^KutnN%CJK=;|(6f|;kTbYPWcQNYP$p$l#q>vJICU%Dt|U9R=YZa4ukGIqyI+qG~w$GHX`Utl15K zmkCS~>%44aW=%vmAqp`J1sP5pZ_Ye`b0vNq;n_A9qU__`I(^Wfs_ zLITYsV;h24nY`9o5DcC*hF0Rq&;UYNsV`P!*+I`)gGdnLwoM#~#gS8D+A7(-<}=MM z&_uH<N!4*?*S zgb6k>h?UH&?8;{0!g;q?7zF^I`PuL1D+m6O1L#-_lkf6*CceJ1i~st(@$v1#cny4e zC8-40)w5(1g+_Mwb9$mAHJ@O|QiWwPI;TIF>mi9CIBQjC5L^spadaoFDOsG?Oh^dd zgZBv}A!ULTjAt#VjjY%va2d$*5aPZ$n-EdFg!E4U_x*{$P4~F7@7ko&k_f(apCKJa)2og$Y4b{$yCt1Xkq}K9&VvN&ss1TA+k(@$#hvz zaenEE`_300&w~hsXyTS8qv^YXymTABQnss(Ktf^9y7Qkm9a$Q$pDTYywvBhQW9IWD zcDTqKQQ&#*98S%Q_IU_rPD_}Gh7H6DtfUtt6}En^13n*!7#?>fp+)j0shaD_*^(w+ zoI_>FpBFjTkZ^oWhc8~n3Pv38Du?+wyf(N$WXojo2j>mWI`vuXj>Mbltq=%dnZl<9 zLN<+Tc2i6SlRsEA7;$yy5Hc&y42W7I_cwYeG=Eoh$yv2of|AP-@?^y;!FRWsQVa;3 zjFyBE6K2W$7R`v7SY*3ommWuOn(X}Gj%;=^=x7LLWao-z`#n$@h_Hn6=4=?Kl4KSG z`Tx7-XG-`o=TG4oWZj#kt{NQL`f88t~~o?-A_I$(fWSSm-$Cq)axX?ZJiL57tHC ziiBYm#yH|l4Hi{|ll@6tDoHeZz6y>-BOJ>2U}gy)5n|5xyeZL1St>$NL0%aXS2%Pj zXU7ww!MRo7^GCpce8T^>@b|xc!+a+G_VF#Bz-7rS3n3jh6W^tqcrm@^)ycq8FqUTv%RpcOZB;L)+N=MFB=FCHy!yC(C)Vy;DV+qBqXTiqRbV*1C zi0P9YZD7d2GaarEF8(ZMyExYakyo~dVo)T8x%?rP?;M`AXbrh_V^N!`f7#aFv z=DPu*At%<)62Jdm4czM)W;L8aS;-V@GKFxaLAtL>V9um~=AAG;Pox&6F!dQt;t0;w zYM`HqU=v3wAZ8NZx{vUzc%6gb?7AGW5@sysWYg72jRo13E)EGL6EUCHoQrUoHjK6i z6N?Jqn{ZRi&4jfmBM5ZCDr9deDa7t8&tBx`H3;W#)&u&vBA=-^A`%fmchjkz@aRlJ z@&akGeq-2`8k0)+EGRM#k%h?)39%3=?9__969`!dp2t7_{0pD=CsYkTUO({d+c*3h zW2C_HDe*fzYvQtzga49PknOhIlO;xYOCb2L7bY9?x!6?@0a<)sMfx$$xQbo5;7vb% zrepX@ED^47f3W<)WiJjpNx!=kUYScrVxw|l#hvhj&!mDC9vO>+?u8i%byx^gm}CIl z>rMz}Wi|4$3(w>Mpg;$ba{vMQ!bo^lMUFg#GLT(lkbnN*c@_`l%>&@9w)r!3CEb?Q z+j3x1ldvN3^K8?T;UF;~e^sSRe)aR@fz+BsI5GQ!6CRS)S`q9itTQp(&)p)OS(R1| zWD682u?0^q4h#a%2olOx(R_RKukcd+&l$}C&WH54XC`EKo_dqBj ziswHNgfU7hP~A)g5kU&d)KWVEMT`2Xi_N)@v9n5+gHROaPHw*d&;7;u#4)@Tg+_K{ z9<0n)u#?9oKNMStomc4rvP#bcd_PZqcGJzMiiE!0J%^=^raP-1^M?owyC@K}o+pIl zP_^K^o#?Yb5s(_p_f{Q%N0wJ1_1A=LU7DETWHW(NlOP@2i^^UJE5I&5-~!ywodkx4 z&j|w&!iFSvFOnn)C$z*Ll;4qT$(Us9*&sn;ccrS@Rk}yQsmYXL7b_({;F-0&V@U@e za6(~O>&%8PgLkCl3_@Z-R`KW}Y_U6>C?tU75Ecu>#d@wHWXs)O4}b^H^I**hUyi^3 z+utFP^VB@KnLJB37M?Kg>$>puJn)&snagY!6B;lUbEevQ84c}so?S@JR4bvISr<5Y z9u31(U_2bfn2Fs->)O+TG=?|2xA0qI1KM4!08yDGFYh}fe!75>tqQfQAiMT51 zmd*;PGHq6}Hv&~H2DJbVe!Ol^tJ<9_741c<%mK0EQORvmliE6v&p@$F-0js+x5hG!SnT(?nYf7uQqp6q~ zuycSHNkPk>U#w(z#0sdPsf&d?KpYX{fHpEniwldCt*2OvL)K!WDd!cSFb&?6S`+E6 zLP1DqjD{hS1zNBft7O!N0)O9Z3hG=72F1{Udp(4_&rX;EWfYZ^?LUhukT|i(G=u=l zCsao3Iz$%PTuLoR2Kf^12|JRUUReJ7ghZTzyDYE-&zv2!g`ME?nQ*IJc*smDXfftW zwh-Aji=>E%Ez+x*oY~l-w`?w({ZxZ%i2&k)s$NL8|a1R2B%clo{|&uk|nnfCA$^*sq_TMO0Q+ z5)|E4GgmsE>fn&i#jJ`h>7M~eveZz;t#rqi%|W6t&pn}%?6cQJrwEOfi1M13ZtG+AY zEEXH8HS8N(Tv$MS2!kVa|^ zicVS?h<5?9s28b1p@x|Ep! z%4KIO1&4AN9&&aRc}FIku&9$A#x6P58|DYGR4g-FlOB(l;KerIEc zUrZ^fc^8iEeC-*QaL})uT~cyLiZ;&yXDe0&kZ{7^Nq>h#J7uKMvs@Lbqsnt&6uFvN zGn$-m_0av-Re4X(*0z+EB#qk!ps7?2uM-nNzV_81QFDpEkFg| zzFl}-FZ|d4@?YqFkZJWA7vOpy?}u2h>ob9E(?BA8&XPK||-_E!U+;#Uk_1S5yEPl$_7~em zrF108c2(4_ao`ISJ)e08nja8v=@p3Ib>vkqU*>G%uTz&}K;>Y?aoF!C%h_+#AUMEQ& zIY2K8sx)&cYY!DPi>J?3WUv>d>p-)xlrYfRnx@zmID@tt^}g%7WY;QtSpTMurnL;c zqWmbrYL1Yw*KEy(^Vt}Et`cf1=w$ZycT$$JBa!a5s!A&O>$2?ho2rfN+or1Qvb5Q7 zpzbFB++i)Ex{EJ<|K&T872R7ocXB46RaLp}A$dYqvfXuRu2y%bY?FN33+=)7CTrvz zLVNgK;AWIhQ+4mf&O17r{4w6=)}0Kl(y|pR)Aw8zf$kc!OVdpZ==-sSpzXgRppL{b zY>@nrpK;B}A~fX*!>HhAXBCXybq;F=6cVJ#v#XEN%YaD2hGdO1&N_>r;2A^-1njUx zV88tO%XiTXs!ew+)Yy3N1-SUj;er(pX94?AYkT2ni>87tYb#(Qs|v3Xe6f~z;GNj% zqGI#w^9MdZZ~XD+pZNN^bKv7>FQ~b;Ek;&XQE`g_B_Xn5#`Y|>cTZQ+vHhv4Y9j;` zguoJ73W&+ur7Oy2l=DX4%V57FZSztxom6@qY`%Y2ltHCx z8NyLWiBdiLrlC5036ja;nbg^`Rwo#Tn5<17I#6u;k)5Eb+~2-O?yD_T>c{xOh$>oc zK_rR!GLfVph?O0aWLL^LbQDNe!%ZP&l4}|$yf1!{4k}V7gc3bRl8PVI6%-u6ZaKyO zK^P}Rn(#_=6+D@U>QhHFlvT{5pgV#bRlbxyiO;jTW(W@6ep*ln(5mLv4%6~uaZv1h zCY(nv001BWNkljI~j`XFl~i+7R8B#pb95!Ke2kJ#SCqi3L71atwo;FK4nQ%$$5jj zClsgH6n`QTuQ4(vvZIM(?o!=xkr14JE<6889aPRuo+p`L;YrONbl|8zQ@od)7f(zN zj4S8E_S-*x|6bKyc}|I1;>ek6h-u3X{N?P4gE0e8tvTo@JKL(lS+_tGL19o^It2j& zwxbB({`$n{=O_O9>j&n&vI|eTNy&d zat6v`P*k^3x`Tu;d;0`|EoM;}OIFD9HQQFAgE98!3hHh0AkhJ|?H}~*umaK+ z7Ro_-Ugm^{8dea}L22~32&b$*=+QF*<@<=P8Us7!Mt`4jj4c?e-I+@tOqO3jb_-89 zbn)l!LmGs$2-(zy?F1U6WA7vygmOwC6-O5k$Dlo8vCGgFNwxMis;`ti)UU)@RLtr| zA~E_Rnkt`lspdPO>`JV)A_bo{HMf#W&0d;)wZ)so?=O1`F{NIKK`tuj`Tlowch6PO zDFaq8$r!td4C+iKKnJsoW725B3L1PC`cV2B-CcOmzMF!}pHYGHJh4HMo^+eQq{8o9 z?6gu#3$%?aQ!`>cb5vH|QVz}SqDi(SqFn=6XR<3TfuxgvA{$<-(u6iXMRI9?G|tZ` zuwjilZ0jK=yGkYz2a7CPo8v1QNeJG7dTs>?E59qq2-D+^*%)|@3qrHf@QeW;*GSd< zT)4)S9kgjP@=OLigr>E|`CE3E&z##i`l3@qY3}+i1nj^6`pfsi@4P4T(s*C__>^r% zIhCqp=4`B8GSQgQDp)d!6jU_W;v2O9Rzf97hJ$-rjemZ9;y?c5Kk?U(A6SdS*{+1w zZWEy#+M=`eDlD48Op19@-ITNju1yTRaarkVr;?8Ga8w}k#)YFPuI*+ToBrDHQg7940} z^uV|2r*Hp}hFLvULyR24Z1z!UfJEK{P(fz}u|UzUHR51^p_9_KZYm3utX|PB)Ww>O zCPIM9|C(KO=OQ~vgW^!(%&25hN^6ZOwzM3Du|NAFj$e@6R)j|j4y&X>sh4dBU@6iD zd+j_+v)^GQuTp(`6n&W8qI(-6Zoe$s#;SaODroETvR7H)yhjiCHG;{5uEh;IRhOjQ zA!m_+rsUV1N$hEN0$u7fRXQvA_juv`XOxos`hB266bxmsLeHi$W#+q6+q@Js_rv&-;rV zMBwxD2kz&_pMU(B1EZ=W1Rapd)UTrE+pv^fOwA^C1&yA^)UaI|YYMWgNH2|T+E!jV z)nRzI#v)gq!pa1q0itskwEa%fo zi=6;s2Wca2UJz)(2RS?22Atv8bY$!^jqncc74xe~1|$vX+_%Xm&zEp^@MfFASx|Pz z#~f&bmIut6Y@B>B$)rl^yLDg_25E(L_gDD3$Sz(=*CWKri|Q-osa809#L>~_=eC;J z5r$fIW*Efsb8h(Mve7?pb&`x`XrVe%-?+PUcfjtuApYfah1Tx;y4$OSQOSx$YYp_< z<4V&btAwLvPhZ_jQPz7$#(opcGNhYkLvEFz_g(cpo2af3($G%72C2`X`<+J|jug^6 zY1(VByKQNc$7Ww?-HUAtQ@S&)xsIeoR6&+Q6_ejxM+rZLEX_eyV(#C09-1MFaz{-O zUAPxVQzZSe(VdgWqB>vp%#^cX`{%#^{=KU1d`Z&P!k{@}RkUf;hi1WSYvx+AWX;f# z);xfF4fxP`&>nUMrm3lTo-h38zy6b&gbg!!;nM9ly|LrE@Hnc}ZQ)J9TUkx4bp(uP z804%>+p3_FQen7tU8;PjL03dOYL>@N9mEvLZOZKlJEf~_bbDSl+68wEUhv=mvUa5u zmS4VJAcLo@(+lTaG=g&qs}Kh~(%&Vv8Qd{bNns>fR8~CtcuQvcshQaU8=+y#tUBl3 z&%wP6{jLJX!YSEWh`PwTg}_=h$JsE5lQmOL(b3OCI)MVE``T6yo2DK8drugxey56b zhg_KUDvvcNNmy4kY(uE5*(0XVwW{gl-d4+EUY zhIx#%SN9D?R*_I=RhmBUU0Db%I_jK?ZUmrpD729`c9uS!mi1`%U(hlHN3tr2RBaIY zk@niFqObk6_^fj8kdhX=;puY)La<}6`Gk~>3=?k7D>fwT2kR^6hneUHlg9PBI8zCV zCI=y8-wqEZEP`hfV5+zm7A7g04wzosSX1}i0gX=04EF0UzkJV5L-6%HJ@{Ku-c-Of zE~?094_H+scT4n_NE>xXw96|$FtxiWI=0=$`J*P#1UaUHp8nrr#3324^QF8&B-hbUt||F zx=r+gi>!)BQ7~*?u8nh7XDMyzKL|PQ!LU-fn-yufE$mqZ)rdz&ADtMSGlS~bV^e|} zHr+FUf-{>`Z(<*k@OUbyv1 zU!}mVsi{SeQ|nclQrLgt#7)iBrXH6YFE|hJqmnfS-Sa~4=cPY&FmX^CnM8*jaMFGj zqDrwN;e_fI%0mE9K(D{?c$!r)8W-FEn$;pvxKZ?EWuXIt`{vvG;({mCMnvmkA?d2RIbFqI(H+Pu0745+AyY~>w?DZ11(Awu zF9O5fAK6K06vw{jSg{wQ#UB3FUBMaHQTi8aLPVXb9#~g3Y@<7|^mk$bSJYSdCesz= z*u9rYB|FKOD6DD4CtNCx6rg`v-<6AOBA(5&D-q}$TvXD4V$Ey;GmruyCZA*?NO*n7f@TOPzrJ>DT3%`@M+PH^XD3MMFD5U|Nb+oYs)*V z`!mV`f9XzSwaBYtVi{}VV4l9lh3gue0WOSjH9PAvn%#m>D}}7Dadi?)Y9BhQoqPy`RfF^ZI4fK9zH-6d`FyC2 z4lzz1lIQ-ypMU;|zkYmnM^F{?hC2LhhS1!DpaEC~@TQrrK0s$DQkMt^4K}Hsc2M`I zgg)qWL4T<@x~bBI1cggI+h};iE84bDY;f^QaLbu6TIR6Lu`Q?_$G_Er5?` z7%)y3fT|p8_Qdlby?ydX*ku`U1i^wh5(eC&y=K96vm#jj>`EVdjcqy0uC-@ye!`J* zu&G2TzKX6qGvFQ!pCdW#;D%n>*8lzqFIL5kJ%km)n4NqP@6VIUZS?%E=1Loq*$J9k z2dPzxlTOYqtRS~wqR)UdH*{CufrX_X_pRp%HG8aHh&J&p=#eO437$sfEmbLfy1}>5iXg ztZ3qsniCU6hOM2fcpPBA{{HLt;N?^S|8E|w@~Qv0zM+_^552R5Ui|Ig$(51Aw0Dn} z!n$S$i{^t8iXWdp@Yi2|;q&W$!%6k96REufh;0uc5^o)eGW)ldI7uGOf=_D<3sov==H_tO?0sKZ=Bp%Yq?W8So97tKMBqsw76RqFXB7xa$o zqq8~I`8Eds250S#7+!FD896cr+bd zp`P7Tz4vBg>4pTmLhhBk%9+aQKRsNUK=vI+d{#v$2;fm%mUR&>cB28 zI_OHzZ`(Jlg7SHn>yF@rw0E%9qrgzvm&Qu~+$(sugLEn48{XqxS6YFbjK!k&#u+1e zc2+yg-=1C%`pcwK#g4)Y7^b+WaSIXHbpax{URN#`>*v72k`T07YQa`9O{@E;g0B)+ zd*0FDHq!1Tp~iGLwqJh#<$J8mK9>#d8$S+GEilnkTGw{G-kpp{R+Uuio_5B$-m{i8 zMOT5Bz2GbG`Qs1#>(9S(+u?NDWfN1R3f&K8m^U9)T1a+Ujr3(Bq;=Ag&+T6OMQ?u_ z5SH2sXW>E8t_&9a@wLsyc>17p zKk*_LtEdLeIBXkwH@j$KvnLg}1Z;xCh%dHwp;0=KH>XxROcsS}ge$-&hhLGe=IDlw zp||QWKtJ4KJ5ZkrQEZB%K;L0N2A1;u%0ijzHNI>_eQXhiPkk%o4)rZFp5 zJ4|msw{ihV6hTuoh>DR}oTc^4g(1Du!zfa7$Vgj)phf$NiCEI`O>U+uwP#?_`XGl& zBbT{wS&RFrt*EwW}PSlCttYb^*VjfJN=eI}aaQ`RC3V6|f%Zy53x8^(gF=_)AU$S*&IDya<57Fvd>9G1U^;Sd>Yqh0)?VB$RMqPLyH7iX%Fl#%F z4bySeZ4bk}J!uxX>KxmkYx)UbklCl%V6YNc?Te&VBh=a}qAM4s=C~&?-n#w@j6Wat=^j!BwC#wKMQTat#{hvjqO$O&&%ryX(|dulTGof$p=_Nur!l9sv+-iVo;CFIlqfb1>Uk#7O6KU@+S*=T74WN|SoDli7Kuf3Z~A=~ zE=nU!*-WvY68xF;73Di-+n|sMJh;b2ke%LI3B}rWW_)9@?c`rYCkr)tbV)lt1ihn- zP@udKX9`jzhSYOc(VAzx?*=ckA}=fG!%V zOM7v%kwZ45f^mU6f&+j$RwK*O7TfnTo2B$TPVwNv|M`#qgRigLBjJlKkq2&i4*42) z^lBb=dwki|PNY>eRRA61YHN=%uBNzZ&kMZG4L65dwrUhxv0YGvb5<~}@$Q>LT4J_~ zx_hhS-#s0Ep_Eq;PK7Iqi4 z)#$ws#c4A`fvOZ!@1Qn^A6=uz@w3p@K@9=R7OfPe)J)n@csn!bwnFbB$T&1N$k_qb zct7^M@4bVpuBH|{t%fc4E}4V!TNLKwy`6OKB68r=K%Ml{axkbur#p}8qz=wWkR4Y8 zYQZt6XXTHb2nH$AxoSYTc&U{wTvtq5?7)8n<5DU;Gk2E!L@1aPS)dA9Y0!gV9j;x2Z)BnoZrVO9^6K5>?k6-0#Pw4RlV_D=}mKZv%NTR=V*_tQXlKK zTVR!~prdUZCV1gAI`pGU0xh#A73}gRjC4OBHP7vGMR%+rbatuMiLVE*VtD{47`I41s9}dB22^-(M%Jg8%G$RMy>UUywaA&>j&qQ17*03;nG|a<+ ze4g)(m$&&KyQos*7t{ejhTLPQt?U=gs4*l6!Uq-HF7#i1JAng ztowke9pzVAkf+&&`K&(gwJCF`B32gZ_K)9x|2~G{Yv$&x^g5BY^iA6g+K9{clX_iO zZoG;KuWe-v@<8NX>tPt?_1@mT5LzhRdaT9)d)4lu}k!7*6ZT`}(jecwoY>_%$~;MRwrSHEt!Q4ghbTk4b3 zKUaFh)w>gfNHfSfRxu}sU3w|`LbmN$@4slXzm*;?t6-+GS1@)nYi~a*hLVznc5>Be7SWEw9(&E>#(ZPLPF`og z54OtI08{`vsm5|x!KA(XVVhF;{h^RgaaD((H>#=~-1Bq{NY`CxYIpqL@0S91tBGY> zMELd;&aF2ku9OynRwC0w;OLPS8+#4x$!xjWQ2c`M+grsNU`X$n>&Lzb8`mN`x<-N7< zSw7o2{csZI9Fxw!PV!G865fG+P+VBCGdpyi8$ORq6kqEhOR751y{9GV)|>2L(prCt zX=`?zyMc-*vDWNvQfr}=)J$*RX|Qn97+jY1K!%kwEgiVGo&IG5I>w%#Md0K5=z=-V zFqd&DFs_UGKQvLsX)HURiE$YT9~0^|S|S{vrkGDYgmTthYa)DNy5oLkwr0wgX*`^= zbpQe2H9m4*HJKb{iupWnFQ~)mzp~v*5oMWeZLsK^8Duuy`7P_qMmDDkICeU z+Ql;j6&R#hCoZ>??bQAU`>@^C)a>Xe)o>a)8qX=aXylJmW(ecq;Z z*a7_~N7=UOZcT!`1>3T7sr}~7C}Xe}$*p>E%qR-JNC$w!pGvoNOWOs=riHiaTT&yS zrY&}rTKURKEeVLZsc?U{ZD~^8SRU(*l36f}99~5UN$h~uGK`6IVX3Xb4A8NlX;}}`H~2g#eOo_5`GSDryR{f^8l`Eq$l7?w+=1h zgOn42_1NN>PZl#P0+TEygY;lJ4YVeP3O+urmcsw-^+Co@4!?(__{IgfFQdJf^9?`C z@yvzidEWLOm<=v3iZ>A$@2ERGoa|ZXVjrz*!PozA`+mq44Q{WqNgwS}P4|ErO|H=+()`4`$)?oGCmEdRy6F z#pq(UIeV6uF~Kpp`+LomLff_k+V&(Aru2UJ7QsnSu-a-)!aKEba?f|N$MK8>=g;Bry%wP?>3QoaOO#^R zt#$lfCK)&j;{_W7i;UPbRMnpM<$7MuzGy7@?e)R;O;g9xA+7uwU~<2%SHkzzUYd>$ zoN0Ry42BoqN5WQy?i|wH^YbSLp zii_v(?}E6)avcrxWm2v~KZdpC{aP77>m3>QnG{npiVUYGpCXEh?1wG((h6*!379urIiy9Pf zW8{F2(eA>-ls-|bgIe!galw-GJ@T@BKMKK|w;aqVoD8mgzRRRj!Wql;ZEmKjuu*Qu zzBNY;^_8dB)$Y66lxH&N;D`Q#TR6!yTSPVzjt3S+p$YTWEHfdh$?kSsBUc6-tiWRH zftK>Msj&1pLkA*eJ1_TaghTr_$}CYW*B)%`Nyc4qz!gol7)<`l4?C8FSW=D*GZwz) z#O283P(tJt5E)EXg2xxuD$_}S^Q+4lmtxJdJ6W;M{`3c?AGzkk3~Epev8+;1*-NvX z;mb`NJ*|QZ;!!dcI3di9g~SY(r}1KO?|dyh^9w(|{_3E14hy$c2Hsp*Pw%R8pmusV z)k@3OD%2|7!y3GjP7bJTTkf`?OS!d>n0oDf&<%&wzJ=-xCLeUt6g3mBZ!7oI$j}e# zC%c3A$vV13L#N$FuB^N^&a7?vbLc4I&2(^WBe8CUPt|)II%4Jf63(mS@bW5uv0jYB zRk9hc?afrMtQG9&5`u%bCT=y1Aao8`8>9EV4<(LOr@Kl&^#YqXQOUM#A6mF5Z%elJ zBr7|;xt=Xitby%8Da~%?u&Fm| zcz2l3^R#<#6(}mCQ5MH<&PiJ3#?Na>)AEH7-3DzmAS^C^wXe%3p0)7sOD9e}+gS0` z2N(nQFl}0BCjjhfsB{z2cIlhHUoW7lSDb^}hEMW)1{b&tyEy30q3&tu*EGnV{uk|W zl~`-TXcdG_56f*X=92eiHXS&A5*wb{2gJMQl^?=85#2T|Br&vRCANjNQ(Tq*JN=yY zP6QEaiuP8SAN$0ke2YdWh;93iV_(3o2+QZEy0P+3tMjs02LHS^xQd0|!2;hyd>!qG z(2Y^y%f2DIZP%`ZDJz9mwQR>M?C>v&ZDsGY!7j#&`)qbp(io!+X@VLgV_{MTez{&W z4nsd2Hr%`8+zXw^R+H^FR2@?3HEQmV{$1QP-yTzTz_0I1%Zhed-*v6YwaCtDTsDQ% zi%z;1Diy?JpMyOJ*!oKHdeLlG!V%YVb1dHW zt0bg7;mRWq+|gdN=S5oo^o12UJE$4#2{-y$w7ZXXvo6mnKejn$tRqshifUib?uL!) z&TiD8v_`SFz8>34a|gQRna6gV(8DNlMQ%!b4!UzYfv+5-mqZ{H=A~^}SnD}vi#ZRy z#tSM+)5HtUOgL7%Fh~tnvc$;BLMA5r$3K4i-c|`!CDH?HEe>4iOJ9hYv?tc~%YtU0 zx$H{VCcM!`fnx0h#xxWwg zVxc2(mK}EOmM@Pk6Qdzv?d(3Bh0pDZdMJ6K+*`HxD~Z|tUM^TcRBqZ;}aBvJcA zwoN47g=nA#fdM?an99rQ{SMHpl5vn0sv9ngnC*?E7`DBKl)*78>NwWz+ChW%<9m@o z=tbUeCOO-caf>teyWQ&+w)?Lff>IU0;;?bSp67X4ahy9?1bs)l(r%3s`AlRmC6u*# zyQ0dTIl=cX8fhE!=#7Hc;Ms}VdtX~eISB7Ntq1u*dLE&gGtky3Zq&9Dz$Uxv-f53= zc0N`=XeP}PbefJXn6`?ZMxW!t=KIG{B<82EgKennoc2OD&c7m{gyEF1Fw~$Ug55tyxyS(Wq%%PZ}jUZWDY^@i56FV<(?Nja_E5Ns)PfRqbLqFF!+ z8`rjHpasEP6Z1)3htg?Nke1cg%Wz!=taw*^V()42wJ7}CNg@xIy$_g50qd)A_2nz; z!7|1p7k+SPSM~>WfmL(xY{)h^848C{9|wi!{ts<_zBM}|ao=c$RkzH=tkLadQ_FO` zt+#my%du1UVxt;17Ib(nUP{r^I=YUuiVxrWwaeYLESm%8ZUNrxP zu($G6^;MO0{@@f#sb!){+O-|*GxYk*+OJ-n<+=O2NCt8GJeAV;uzj!Y%2YANXwH99 z7NaZOpP|q=_E#I>vJMspF~EDv-r=+vy4A@H9(BpowAo!HzU-}*I7FNUUG(Rz1eX>p z+U&o%H*OwlipR)`JUha@zM#nr&HCA5x$5rAj5MJ!v`u}n&`x5@_8il_+gv2KCqn4N zuxAvD&`8eQtc7oF1WF>nJDYItc2V1VGHN$Qor5_a5^B7Kh2`!bjo14YHFC3_NFZc& zB202jFY|3JNADRaXXZYCReX-V8@;Hj#E(iUcc}@2y^wb|k=Bw}=|A$>yq??ye$@#>Spwlz7dO zt`@QdMS~Q`tnjw|c{voPDWvzMOvq@}7^p>NXMo&@uIBiA^1XlVrY|RDv5oRh7hXL!;Hkasz%HD;%ry(nuvVBKEWQ3dM<3Da0ko@f zIW`fVn;2F0!XSHdc->nXn}m5_;5)eU4e4nXh&C167B+_#tc$paiB4_>xYTY3*hv#8{x&X;2^tUHfyA1{pS z)%1Q5<3?DPWP<*`z?w!9$@ zSSTSk^q)ZwhI_zRn0st1Y17f(WZrY~wMhWFgGib1m5qJ{OgZ-6pZmXCqQZ(%WP9{@ ztxBclMOEL$k2RxO3uTy6B0u3gC#l*+dqi*1tJxM}LzsDn<`#w0R4*vZ(%Key4j{F* zJPw|N)eDb6OuX2vr8Ez0rdWFn0jYgB)(WgQ7T)fdwg#jenyueiRh;<|;;`6{b^<8I z)><6Gwq{^k?Po z5Lyz1iq*%?`D1UrBRG@NeJ>eLuGdIYTG>ga*ZI;+5djQ)HIXK;^+L~dGEvu?N1pqF z4eh;3;(-13k6*uoTl0o#JJ@CwpQ~z}6-c&E#iYqkQv(CZgdxNE-k7(2u;bgqni>vsFdg`dh3~F zC%7gRr0O3>*HPWTqSi!v$X}N6dhAu$yQ)jMD|&7s>btZX%?u7AZ;?D+YoEF z;$DMy1oOG|1dZ4!j#Vmcfl<1EdJgA?JvqAJknI&49dOZIOc0kBI!L_R*>4+;vjFU6 zO>%Y#hrwZ-EJAS6)n?%pGf8_*YV0LlH1j+d5l8X3ZDtGNsz#<+Mzs;}tQp^gq`Fy+ z*lQrHmSxr1Vk}Dinzn`}_kJv*0UMSp>yNU}sl6%N0FpKzX8X7PBpe>s=r>cQii782 z;eC&MS^5hXLS-Rcwm-Z}z9Cby0X4zv^}=<%+UrcJ%jNI6u8U_qDZMXy!Aa3AFEMKy zCiE_LRl}SQ3(+Dv@WrSpIs939J%@qVWc6sA0jwhyOGqP(SF4bHSMzk}3eGylxDr2| zYi~;P;_k;@ZohdDJs1}nH2M)t(gOYe9?-rmvN;KV{5f@yo8jVn?}TW%Y;H5#rc`T`ZcdJdu&am}1H3|3#DErOFJayW_7X1)Z6 zDm4I1R!YH(y36u|O_R%F_jNOrLO{6nGwnfY=Lxsv-_}FS>EgbgIQh z8~$ngG$nemS?1=x(~^WIazJ44$7?+p9`+Z|aMF9%y(^E$3T; zIMnnhTpV5nAp~; zoF!X)ExGYsRl(XOp$-RSlx~_cLq%XoN-kZV=inh=3#qa#Wv>~l3arS06Y4AKJZia? zBaKyl35^Ev<#(3|76Ea#L#tvWldblmNk_7>D!l^UlcsFXSonwm4RKS?5B}7BzH?@% zOU^lPJ7=}Ebjm&3O~1GGL3H~8b+n*56lvM6%Bv!BznP4uQ5g-HN1>Hrt{S4tWaAJ! zpzK;$sedXsDG(IJycLW)6rK_(l*$=PXGJ>UPNQ6eJoo8JzD9siV>83}jp+0GOuC(f zAc0f-4ZO{%8M()VNjCMD)fGnzar@HCP7{Vp$Q7MqC@dIl?TVKTIprnD@3Z$c6WS-b zL@lbw5Ev(`yQ1=NeISAEgh1xE`{L)x!iVJPJr=0}_S?llr*-iAy~WVt^I$d#ICYFi zt@3Ydby$}V4H_%wBu9)qlYwJ@7Y6AuqX2+J={7CiD$qzP*!wFd9wW8I#Vdxn@H&Mo z%6HbrINMPqQcaXQ$R*AqPTn1E<=&$!WpuTuSJpiSq2et3aDEW8QYuJtOUtv?hDgD_ z^_)j6tj(D*;i`G*<@lN+8m)Ku-jvdEFp$gho9_a;Bk$Rvm}OF|OT#U5q>M8^vKvXm zW$K(&7UD6WE<8`>wA?30%^I?AWU`c2n3JVE?q0c()%{0zRwWLIVKs$@jO2?0)2n-M zR~Qo~4VS7P@JdZGEC&bi4F?KC3MsG*8VUgJ+*_T(mT_CVQ@P|j_tdQ4g)+bxbIrG; zw{jVz|J>}1vRx#Uj_NNfqOCx_7qTcd46X2brhAEsJm;x=Zw*NASW#;)(oN-!X^F-q z5N^i~iRk!n#*fOtmebTI)!AeLFwh)2;CUmPLgs~uG|F6%cj_QNi*gUowY|4V&#*+C z$Po{JEuHfcq@z{~@!`V->mjwwo}^W^6iR$QkC+glect0a(hjuKDQhjQ}+g=57FuYr~w zol`D*%?rksjsX?05-Cn4Li!&IkT!b_bgD(M4RS|Woi=uU;!sT)*rS#*kr?5!!W-MZv} zx3eP07{{aH9IFz!TAf6IfB*aZu9>w<$azvBVQY&YM*`WT+jhHIxF6R>We++nI|Np| zhG@s|T=wt%Db43JbL+| z+2j#)D;jgJUFwo zJ9AI09^i&6B3TgQ0cyf+TTaqK|B^+0{nqj>55-$!o9dR}<}KoA+=1a}RgqHF5&|>j zt(3dxxuxC-z&o8I*?Tii7v@OL+a(5VT4@*)bSqeh)<{8WbdZ_SDfO)9Q_&C=%70fN z8DDBGk;5KK?U@ysqhfKOQ>PPwY6xU4>en2l87Vl)g}?fU9sH5tg8 z9@hxoF?LdZeaP0El1sG@O_@+qiKP!SmMQf zDV+YFyurp6>Ghg ztkA^B;?F(zY`DE)Odl!@@M?4J@PTEnLH>T z^Rv9A7&5gAu~97P*0urf8<{1QR{#w-Us|zJL$EVZU@}ZI$Oc*oEbCf5hBzBsPHQ9y zYMc3pb(el2Sgm&cx)eEB*+qGRDoa6!g?vQI7CQt9x!yWQ7~|riA_M}3`Z1$NPSg0e z@SStiXKPjNHqA^Gsxu_IR*8mQdFQ4k%5Kg3@+dI{=hQZpRJWw9b%@q3*075?5RFhW zoS*Cs?shIP_nbvvK<}cmGZc$YTf0Vg$(Qq{2DABD?5bAp2gg#i>BhHQEOc z^6i(kvwjkw+baUO>rjCuQXSqSQjL+d&PeGBLJIAynp#vZBr8fOCVN+CFIx1(*W8&I z6M&IAFW9I;i951lNXEpV#5+(#qn!8bUg#flJOi2~ejA-(mK6wQE=U_2N>mm`f`p2g5IGs~bSuu#~*>U%IPGT-})0_-_YDP<}B zE-6fSPI%JSPmzaa>%af|AAdx+JpPo4$*)(C5nD{tU2w2vCcmF= z=>1oL;8a7F?n>&4NVA@wEC2rYzvcVEjznWPf$ilf5G92(Dzn>1zIn;~bX2+5Gt*%h zu-0%z*P&L)WF3x!kz_6E`Q=g75Mb-a3-_UrphhpkX&%IX_Cd%LeHO%Z5NyXHZ$VWx zktSTIhqQxRD&4IWM)U8U{u_1+WfZiH&kNnVbdCWPa7nIJXxXWQ*LO}elovGhy<%`N zx@+%5xAwgJD$s_@y>jwPU*;Hb+K|i6Ol;FJwlPX{;n}ecHskDljqf|6(K$8t(Kf}( zpfOM1xdK~bV`sq$S7oTM8yH|t9TWwQ<|_1zyFQw;mNqj?X4n-jNv1Gb$1c|;iqD(# zdk-%-Ds@pU_qK7t=)hzsNCxeLag1-WU>JGa{JrVfAW30#9UjAf^fI$bv6dWJ?K1-U z0L@>b2nZTfS{;_Lt-E5H3!C;ArC7WI>gaY4uXOa6@{-15m2s&?^V~{u{{x}Kg*x{j zhUc=zZrqW)BC1!7_vs_cOK8`~o1?nQdauy43mgrTRvWvaACu4CvcEpER!4ZNitGxD zx`?JW+0(COP0^-#8!aDTuHFcJQ@O`w6Vms!Kl1xKm!BJEpo?%K_XN=G^^tQPIk+Is zedKB$YZ+OqwFLayB3F;xBd>+GOYTatqp+e@R(|u?O|ZJ)C6(G~W+2?UjMi$*c2+4D zyArK<*c^kYgx(6C>VrG+8L~v4$IH!0b;gtpb2&^IaNu(5Xh^&ON-qewNz}nxd+Ny{ zvk8H;#!l0K#xaoT9)5%<>Ml7?bW&5jve(L>8Q?D%AAUBJS|Q$I%B%CmUyX9%$f^Rdq())X=%1U#$6EROUW9##_P-aRIqu{xXqX6jxM3e& z`Qzt2@a$;rFoIs4cSiTN&r3ed1GXTGM2{?$>!_YtUBB=xsSa|CbaMPjPfm>N?lwQh z)Z0(Wv_+=fF+9ywOtKO#Yz(|I6_2V>Fh~A*Ij?rS2U_ExIhIXB39%ts^7I-#MrKJw zNEV4h?U)DQg%AKiPjM^#&Ou);e~UB$y3jf*wSVajiK;u7Z5v(S*jYhN&ynT@0MJ5S z;khqF0cj|HGXLd*pnR*i zx!0?hqS?52y^by~Q|cV!yo>1wBPAS~a2qmkca@EC)VWt~ZJz?WGJBDeB|;uQ#dr%9 zL=-GpTjlG6T{G+)s!R4gx5pcJ7)#}ZO4-pAo%1bgeZ(WtC0~1Y=MXMife8+m-`~BU z(UC1pBuIw9b2bNwZK;~tt;yLN*+^A!V=%;__(OEE>d22^-nKTwNDOmbTpAn&R6u?2 zPswr8f$xryx>)U`&O|~Hb7@U?V3EA~@KlTD5{)r2gKX5s5*kwIJ0n!ffYp(s^JS)s z-T^$z7aGfRSs-^}k9B)TlYulMVRRZxJ!mviQdY}KPX@L`$7QW!@h1h)6mj#C>C%tj za6uDMW4gu7hdkR8JL4`MsdC$Da?iJfttPH=k*SFxuPhmg9hea50{on76hV+#r&Pm|l~%hWUdkd2tZA)J}pS!Cp-)N4FkUDocX z;HuvJmVbijmJDzzx3jn`fgef?iH@tPa>{V?*#%e+1)AA{DiVKpd2Di&x)z-$1GCrIH(qkr%oAtoUn(_I_!DCB z)aa1o<5Lhzyg`&=dtO+sNG-Kd%vP#l09zxqyCldp3)79l0>Ao=68w@U-YkE**;xADfz0G%!}*8nZJsaD#qg=X{%ko z)6>BvpA@|9orE`8`$WcX;I*w=?Cw?_>}o2y zOx9;N)&jrR{#YZwVS=RFzs7JdPZiF524u;|tl@Y}yvPL;!NXC&z2^mjY)Mc{#BFs~ zrecNBP+7?G7`rp;xp!33AUg&M2a*VJontI{`82o-U{Ad4Blo6)@m7vHB;+F?AdSQa zs3YRthPe=lcXEMF<{%q+)rQM8yRlSukj->`*l^7B`N)8EDCt zXm}eSBHO!1&g5o=ig-WEwWTXar`JA&GWBy#v&B4Q%O^iKj_q~H2F^rcVQ;;NRxFxaRsaAX07*naRE>@+2g9tw zh+5~LGurc<%1&lWTD+UF=%lPoS0AvP2^+2cbtr1(I4}ybvy%Ej*|tP1@-jr=3O{5n z%flKx7{hGGC{%U{?tDq4UQ!v5MdCU*9taFSqw=q97#F8(&&BG&8^~^NuW_NZaT#&s zU{F+*72l?SvcZz1qSmfaWf(mD1tP4AqtNR;iYYUtNGqjqnb6^$kVhLCD2_v)^JT%| zcWNd(4_wZrbD?=EkD$ZHh^MhS$ja5MDrLb78MNd8I8q81!u?NoS5W53dojz;fy%wC zs#pEoUWbZSLE?Tz)`Brt2ltDph)?YpCM$|gp%{K!SB$ctZ1V__i_c}Rih|~HwTcyR ztilC|ZY?bMbj0hBjz;bB*!^l@Rb;T(c(|6DhwLaNkW$O&SML=4DP|c<4N<{ftDf5n z564BGy~LC+_q*-T>!$HeYg|bs=iGO+nur~~mAxP?MIb=GkPhuJIs&|lDz&=^U`#H~ zitKi!yCf!Tdemp*PxVgJiKcz3zzI!&Djwxd*tJbooqWq+k2=y@Jgj;E^i1qtic zwBbMJ3`szgbat)0lq$V1T>R!Oo07tg<*b8LTP4K9W08P~0Gym#X4RNefT$d5K3iEG z2wd23;VuJwFu)9QEL8>SpN_I+9z#CNBdn?R6Y(1TWwhrz16&!G_ARua$>B(I68L5T1969H~s4B;oozv6Z9CxQi>`wG&+n=}>gi=QQLogK+ z%EA;US#@q`LWb}GpUK{}maH4%xAyAfK5s?VE`gFN{I}(~OKh z&x3HkJU5b@{y>&~<;cwxSKCF`c+P{wMU`LslZB3oe1HGWe+RDkwYJ!2f3TZCOM*O) z@GA>e`TU{~b%@p{BqWUKUdYvj$z?6WyfrtPj~_Tua}iv53`JLsx{QlUd`YJ4^ZwMcW4L zccrIhpcJ}V^OsRB7vjUMK6pza14j`s4LQe)w>Ud=d(RAdm{d_gYqPdE5*pOciA5(V zI8{~MSzdgm4Y@3mO^v>0^zTK;kc=qS!evxP)FYnw$;>i3#{0Q&Kj?_GqEq{lNoAi$ zW=B&Zd<)3gGH3o}%XSt__P&r?2-3-URE{y2YEp35%)n+c<`@m~qR1u=af?)xD z-iob$M^$Zr4h^Jij&qQ0gPjlIsw(=H?6UJ;TB<+Q9PR3ekf6dSb6|2@lzS$~d$nV- zOKEkpsBk=IRGe`Nkrsh9{+Zhr0Dl8bgE>QHvQfmaxI4J4E=IwjQ3WANVlf4$$~!-a zv9t=&h5(uwz>{1s@F+MC?AVrl3ZwJcTYi0giXicQPlo4R7b#*x$a9|D7x~RU*s*bE z)g5q|f6!8slckvHRlBApfB*G&*}9RfyX5=%E#J>~sWr@E>?oqJ%ZZloabRS%=9Nz7 zuk&0BXqR%o*~RDxkl};B*B8E&bm(@TMG22_;HH9QtuHLJv_33W#HKdT(w(<0(t$V1 zDw;LeJWxwsI%J~C_O?^#I@a~pj)~E)O)#FLIUTxse|Z9!@4_(!zx=h- z7?*K?2KGTUF-fUx?WgcvWd!r>&IZMV=~$ezYgs#UIkZsdSgm4uaq3EnLM+ zP%Kzy;eoW6M@%~8cO}>vRsc~wOYGKnji=aUu|l2uM2a@{Ua%%ac~{)wWpS@iHNr?W ztZB%V^WJHWd-s2T@68;GLB%%9te$kcdn0XdIdSj9IEnBa>oV+&lxN2B60IIF{9xog z)zaCidn_u352QbNVa7s0RN~&3j6J)tfQ>Ip1jqc+u-skikBDCb8rB& zL!t=iVv3Q5Km{GTjnZ0PH^;c1TaO4d08n~uTVj!y;05?{t&hmf_hze=BmevRfyhW? zThd3E^F;Uk(oag3MI|4i5c%34F-aY9a(PV7lb6D;&j@~?z7EYEmpq^hL%83eB_+of9J zgH{e4eTN+ zRh%JO-o-=V%_EoU5+ct%KwJRxQKLSu*icq{`7>Wsmp#^~&?spv_v^E8?!?-+JXKXTqM6&%5UrYv@mUNbVAL2?`e49-#Yc`iXa5XT>cYh9S*`q>5u0j-1n;Zv zmKo3C?7jtLbbQzJobc~3Q@FXM{T3K4o%2*i4HNaskA-B?@Q`z{4*_b!E2cIoSS}E| zTO%4Jrsy`&S3C3GAhTVTND@I;y!QsRf#Yv$@p80R=z2$IXN+$9Bj|f#NL!DxSD7kR zJvncd7Z8Oz>@tZ_U$`*RtCrE+>uB`#U-`?#!$y$e*=j{o|ki?;rR4#Fh7gD#@+<@a=_` zxG?+GjCYwl=VIKFj#39b^gsXpv%dJO@WHP^7f=c-F7kr;6T2z{F6vu3=_BP&D@>}I z?Ux}8Xp}j^kVbse8)(*}scZ+WZ!_&NF61(wiiHuf@J2B{mTO^a*y_pQBn4;j3!nPAAj2M+iG z=HYVg|Jw?86Bq+_TrGAq!xX7wsazU3_(ZX>siur(md*IpH zZOrBKtpfR~3?zEa=UG+rx#MM2iZmJG0>n6QZ1eU;bpoi#i=8i(-z-t4PKIdN7zL|z z6hfXqBM}k^bpwh-RQ?igP@}(*$0GcWjK)wer#nrirdFWG92;-(dx}0#0dDyBlDjrg zE2?7|-jfg?pv3H=;qb>&-$qJkvBYfh^lpubt5^i)!DSelgRb-~Uthn39zU|POik9; zFTg4ZhwP{hC(d4?#_ttQhNxX0H95LpvL#ePn*k)RC>WFH{Mi*O7){T~R&}ocTR^10 zg^S{*$?#^|l-Tw!xsDJPkBU?@G5LPdli;ScNaLQOMogGnU#cmr9o>nZKT?6lc zN=dkJSP@|C{VtAp%^Af$(W=9(w)#z-u!nIVgpTE16?4C~P`-$gp z`Kwux;K`hOuh-6k(o?UvRBa<0@h&4B&!}FKc)ZJ`QW*hp|T~NUe#&9 zZ!u@^Yhy=%5nxJjujs-Idoo?eg~s2bN0#2Ly#PV8yK*ipzx1MgzQoTtV`1*aD&zCT z3qa<6<&`;t%G6ewz3~@FAW@bg1Dj&I~6go~%Aamt^AQ6ae_9I^g@T%JGkl7vv({Pqf|te*Lez zLXiX%7DuXsp6HjU8Q^R5)Lra^EGpGg?scFl4%0GN0&E$vvI_|pTU%5cAZR#CRv~2& z>uoU-H}iA%ds7as8-=vg&s|IQ%fbf*nYb*d#;VBh#_n@;9g(Rr7Y99+uzbJoWi|?o z*X0B2G*F2)Gb5S`oer2*5C&YoVjb?A+$}$adEzeR`#GSG_NDOHi@{KgzT<-R%lM23j=Lhtwy1KQdky9*C- zO}De@D%mG_`91efgVF&l(sUJzsW8eC63)?9+XuN!B|$YWk5z{xGo+IO{N&Kpq;&cG zu$i)l%}UOMYcWFSGsiASeUp)u4fvb|h@+m`@sb0>fF*>t#m(wTrQ>vmJUDO}GLAQ2 zVk?_j3M6PquLipT+&FimX!Qd5P?OM2|&R;gCr0CUS7T}_86G3%$5IJE;1 zl^%3Jxk4E(CCKCFpVgxl5jPJgtBy$rJThQ>vf(|6mq(z)5Vu3FxJTelLTPoEHX=@N z<@`)l`jRW=Z51XMb&aKN6in2M9>AF9`W+?ZyKMCm9ps&G3GYRrZ7_b5*XL(2tQxv2 zB=~vfq3~cfRG*AA?q@HssE4yTub9sH_St#b=$7$ZRZa+sE_81+6$fcv1P-SM8pU zpZ1V=jxrxyG@>fM&ylasM-02}8!nq;3yw$p!fm-s4w2D(-~3b+sgR%*9-*oNskvN; z%88Hz;~c#kA=ABV2ruc1H#t6B(r>x(h{!5L1-%Gw7cegboZDm+p*pTa!?9?O;gRU9 z53Nv{mdfetpwOC|=Unh0uf)tM0$9e(SVkteOX4ZYn8$cmZybkAZ-cS5z4gMg!W8Ui z)YHVNrt4{5W7DM>Rj&995FZrcb%DL{gW|H9? z9LtNCN)!rxJQgZpG$UFeC8Sh0QgA=}A*uD4bN4rNv(VxsH%mVY>xTib41u^<*4m-3O0xR|FKC)o=9kr05{@qp^h{ulLdXVzq%npu=&tUS6s{n4 z+LX_tq-onp^+JfCs(G6N+-XV+dTr%f4ev24lCd1oq`zSft7(F&h8_`f<><&MMYGoE^9yoy z(GllKGVQXAi{t^PVjWc$p&47rV*~+hn4#)x8JpPWoJpQ@3Lxkl z%f%1QQ$%$V<=wxRQt^g3dbi6KsFtFa;yesFUf@zyrf&id_MJ;-POUvk11}Zls)dTl zOQz#fJyiT$EFST4tj;}G>6i^tlUa!e7Ii(F*IL?g%$MJ_jl5U7`7?cugV|*Z!DC(- zB~Gd?bHs6Y*|KsyYXRrav{canbo2-U#6k^Un}36%FBoX;MK?z849M|ZHa;) zy;{9AJBAw1akMo^zCD&&xGa2bxZwWIv7}1wPn@|bct)reQ;c!41Be!C;QfIftMbsP ze(DQ4Mv|hmUj#cu7LNjpKv3q+$c7kTOGFxrdVasi1mQCu#r3j0aOGA+w7X`85Id$a ztyaS+SB}`ViYE|^v$LCCwd3>=3g&S!X;WG23lChp3OWr^Ya4dCB?-JC?S$7b38?t4 zyL7lW5c+&Z)NA*#*Oq&Zoa6AbEm@{07O}KPT&^KmkfVND4LI51H2&Nx>uXo!?bl8p zpu5Y*c8*Cw=_MH!7kR=X2&bBX6m~)ox zL>Ozx=d%hGOy2?z-it9*s5e8VWV4*-=OLj(lU_tvZSpHI@QVBfAC)I%nhr(Bl=%{Y zUV3(TVR(3)d!Uu~>;O2s$IqCu=O~myEyKw&1yTC&$Jr4~`u5~=2sGG+|{iT*%_jEX#7wBGCt zN!=x^FvjIk%Xp3;rBOOopI3H2Bv5xED|RaIR1pO;rJ?NPJG4xjJPie8M-6L-h?&0p zG?mZNi_i{#(-sM^J2zRPvPvS_rLV!&ee1wHFd65FkF$M!ecr6z`Mq;D7b5cceAT^3 z11^8(a}IDxsp?!1+@z4eYpmsx%kzZFN2A zCRJokkrsa5haCMuH$fTn>@a_dT^NN-%YqHE^uyvwMPzHGP;UD`Ms&rscSV$)BM3g_ z(eBkC1bA|)dBN4#nu3}@%4Ia7$W$Flo0Q#sJ`09Bk?RjwI**(MOy>p&EmF^~(wpe1<<)N;$SxW^D6U@xcJ29)D4n zUt>S%guGF8Q^{4PNo@v`23SfLzn?OoOJ1|@#*XKA^4Xt~W^emc;Z#!LnH#Ux(E!pi zT9u_mho?vD&5S;?AT4#;&I{mA~5^^Zh#vvbiL_AQoGZ2l7An9xV-D)HG#! zWvWYyMBH-6qE;POceCJVA-}z#t!1@~KGab^+>kswu#qbIFNq7LC5Wg4-EViaxJ^7uaAc}|+hbH8P&lmJ64pN;je|NDRbFwI0c zKj{H@&VeCmRw`Eeg3sErKRey(l;5d|Lqe!EGhe9SLcV`LxB)SHMg|8_-(3w!RXU88 zTzl0E!`m*>n~_Bz11ydEcWo8k<4(d|}`;hxW#^+d;`qSQ%u)seGc zBZju@<3BtRQD#2MkqhNeAsPc3NW-S`(zlnFWTardDm5wfjn=_iPTcCKhKzW)>N!rG zU5pODf{)*Q?pB!0R-?dyaL+VOpNi_jaxjm)#0EnyP;%&yftt^~61^*0qtlkqLxG7r z@jb$`x+>ZdBLC~}f5TX2 z)eM&gAL5V0Z$h=i^QHs-;`{stmOHD)d%n@qX7zGFCG`<|$%nbo8D1TOLPp0bwIrD1 zq1k(vZu_$~B*)XovVcpm9x>H{kUuwLHo2&sP&r!*vr(%t0Rd)Q;^ltkcHX*V8kkVm z?mZgPfuClZOYohVRnyQ(ZT-;I0Z3yQD{OgJzf-BiFs|@EJehXZhB*iubVPtkFT3fc zCREVJlDZE+j0nczQS@MRCl$9lJR>$PB97&B`a=f)>jE`FZqgbfy@~9Qp$2P-b?-`E zfJ@Z@O4Rz9=J-z~USkH+hCIetwpW*;ok8z3nj&L@XbZaAoe{;-`4lQvoof{@>^O+w zkq#s~_q$W|w7+ZmZBjHvO6@MWF_TDZWCJp;EhwUbW9zrOo1e<{gA*;;OB~y=OJ}E1 z(qL&08uvl(&Lm0#&D204!wX?zT5j;cox+GrtqX=*L0gGDYGFn1nR1jK*rjNVz@1UO zR69YTc^$^TlD;b_*DaVKBglb{XP1~4Sck$9U&P|*fK``(+gpzD+80)i2k|h?!aCST z@?-gaF31e|4t{MAiy0~=MSjguV2C`=Q?v=WK72hm7#741DAN31*%l^Jp)omgJU#=JRRo$Ku3*@d24l{J^eolV`Y>^6-_(|fhC9&1}kggd6zUf2eXd+5$q zd?sEw?5p+$6_|uezDT>X>*M7Ju;m+mrUkv@eBZ}EHzAcR*-f!Wp(-X_VC<>pL3Tlb ze$G>#p{ZPR;pn%~C>v6DI?0v5z-4s+$q-?5rE5h`@G%}$S%a-b1K`d|vp%aV2oDb2 z0P$hc5e$b4QT|!r!G(Po-~2X@@@FN6Mzo({fuDmopLW?J1)eIxV3F05` ze#MgBiiPa%HrV3@IpNENrLVQvJjBc@jHQ0o z;o9futNjSjo%6i+D1zlY7$`4RkvFPD^TDSSkMm$EvzT^R)%j|IF)MpIP;YpRsZzcd zm0H-D=T?7?E>w7R;5A55U-=jKyZk-Ep+bv`Kau!LHi}ETW;?dGmzj0JFsqx3RjBgr z%$mnSaiKE0*nJq+{)1*(hSdR9Yh3!+`@jv)j+BPC7^9zMHjpz?ys#E)^vIDo_x3?p zdp7XrEVGt<(#b9YSF+0Upo_twb{@O zy>^8b)qxMSB;D=Fi2Ecj35RlB64qTzMZ)4|B#J{RlF)VnIRq;k5dq;x_F{g=u4GT{ zI8reQ+U~N^>1SlNhP+4B7q+~fNNNyhP5sh8q-Y@fJwBFvC{LETCx^ z%64wZ5r^^iq#75f`|8S59W0wtPZyWx8;Nim)v`nGh!^_Y2OO6r+f4pE2}=85hM#|- z)`F>W5$ZPXyn$|^b>v=ac{)h{S)~N%7)ymJA|YehzE9U~F?yN-*SsCNgfVBP`;sGq zQg*5R@D{AG({BSD$48~ikT+E{f5%;6MphDA8Q{dmv=>srq@<&Q?nT+6$B6YbD`Q{b z4@}GWBr~lrq%N#^G4!ZNE8k#tI_xOgD`U;)c`bt#nIBs0ElPc9Ff(aYQitPc{aL1v z;ZE}IkAH?$v8@gxD+3->I8n>(9%E_PH~buEu1%6q+jZdQBbZq1Al z)u9I-9qUU&HDYsZNKJXsv1>6zl?pn*9^-;TRmqOGDpOtzeW6J$YAMmK4F`U{^W3^b z7&unB7uYQr#~6&>46AO&rJt!n zsLE$;kvKUWiIC4)Wg6u;>$y)6I(Dek;zZ|R8d~v>5uUd{`6(ylAn4u-K6fQzCECMG zp6{nP_KKJ1AOvVFlY66}f{o|E7>tS*kC)Tze?I^KAOJ~3K~&3|m1D-#uA18~`;ns z5jm66%{$IgR-q}>xX7R3h-F)v+;m&YW*A8uQ6rL)G5akgOotxn3m~Y&Pf`Mb$vqqY zuXiLilrXpUaphr=9!ctOZquaLY_^s>wrQ{pcuS}E1C_gS*E9i-(xW;UK69KH)uqgC zaoKQ7$KY#`$pCK#eIbGBmazhPaBDB^^QRS$17d4S>qDc)H5>kjd+uL$L^8x0y=yWY z&VzFmG#oDQsbqwOWDLqQ^N&PT)K!kAvNl}gIp1ZF4m26Z)048-P7l}r^I!kY-J^Y#U9{G6o=#k)P zR1n%$|Gk=QB$5Vf5#)}oc00Y^-z?A_7tyV*K24Dbk&5=E5p>9SUH9Y>wqlOv+Wb5} zd&#fQFS+{063``|^{aLQNu+HPRS{-|%j;7(hOQKEy;PmAk$L9E zu%r;#>x1;b&Q4I>ve#GT4^*Y5ZJ-)ylYb?|%6n`@l@HYnFSQE}kDN10r<+B~3uB}( z(wTm%bwFC9U#L(9;BSl^Y($(oJ}*KYd3hvFvqQ4x0$-V7QFwbTEQXGyw8Egt#q(bK zM?h9Do*lchUZA&+pgbkJyAxitGL&)5PvHoJ@7hu=iLVZ`<@;i5?*;dGjfL-}H4Ac9 zWNL=AHyjyIkC>0ww(*P~v6cHmgSaQvs)C4$+s<^T886OO(v9hfDdfn=bK$6vGW4?2dc5{H{9Vsw z%xxN3IZoD45#92Q?2SMf~JKF#{Z+#~RVp zciD-GG+$kOJ|xjH*vnwNnzQ0opjdfai15e*PepDKcDxWbL^rN zz7Re;C6UFbXh|b)6sdxtF<4SiAK`N|WY)|g!Rf*l69<@8V+Lb&cWpSlAw5Eahy8;} zxx(lEhh}Jc&j@I2zF8AA$m^?wL(*@8D!fhNXzhF*u@?%kYUq0^Vs+*vrbzd$i9?9U zXMM0{@R|~20pQ&kkA0Y^$T=>*{$}-DeZGEC{e%1uX|Qs|6jnuGd}Dy6`dM z4lA<;$e+JF9(f5Eat&@_FSrwUw@xDsa&^AQ zr0trFc+f7ba~GiULbO!CQ^lR?YhGUlKW07;^AmjmoY^X>H+DsDvz?TBDgr7o?4$a^ zwwD*C8Dvua8;&$Kb_RyuYh#I6hft*%{TnvUdBi+eUi|Hre*sS9>YyV=T@xG^9Mnl(o9ypL7Q7G?lPsp6G z%OJe2JDt~|I!2oPJ9HS-RORuIVRv=U9SDmY!!(9HBU9%^l@dpV`K_})=mk(w*(PN| zt5HcQnqv3_+GS1UnDkmeY+H#C%J?~qouo2n)&)92j6p+2Fx3=7KyDfs&D5x?xK2Gz z7M&r+(XaH^DOMyl}ZolamB z*OK8J&96qe$A#L8i*hK)5Z{nq5IoC>Z%|-(6mnl%SoQ;VAG{o~Phpoq{|;X(?u@pX z#5vh-o^#RZ(Vf```VQEsQH%Bi{z}^$NQ=2onCdGTW;9zmd@-z&>iTOX z+UQ1JpZic`v4Sl{)A&70i4;AGxnMMjc1{Bi@S5SOj8wSX24-Zjb+X&&YSM7k?$Gjk z$UB*lW3#C&+pNlCVD!gcD2OQ^34Jn~Mbv_;OomW49 z=PEnvIbo0wsV@efhwY8n*tgStt)Z%^&udH*N~zcBbTYL3tX}(B=zD%XdsC#4N=Pr) z{Fs~$ZktqS*OUYk9p0U+Hs<3MFrQ0w{&TDHU!uUM6BQpA8L?}8kpz4WFtu_^`0vp0 zOZRDwK-DnDrE-#=fBrjkj;nSus1|&3aNus7Ob{-x(nXk&=8! zgu49eVzyzjyRG-XK3;l-Vgz&0;~9(t*Li+0ofRP3IT!xZ8&DM*Kx|M_-BL-MA5>E> zRLEMr%ofuhp-%czOO<(HV7F@_4QSpw>q6x7VUi<74c0r4ky@CZk*qLkfv2rEFKH$v ze`8AL@(93p zEyZ4)r9H4C_aZqF?qmf}kN(=IQA8`%0+lqS#T0T>;FNnwj72;3F+icM(P&6j(2M{u z`5$D&EW*Z!LWc*bg(P92JW0{MlzgVW z%s->MFooi$vLx!?Z>P6Qh~I-tL#aDML(*9|Azp{J;Lk|!)_q-IQ%X(VT^n(w)Pu5M zY?ibf)Fs9XdufvrcrNj|!k)8n@VtL0nH~gZvgQ^-!UoN^3MX~Z*qw*ME+iMZVsYU| zIwdmMmI;cr3zq25*%(WJOdKj&t7J?C?ONdeiOF6c2{SqV-Nk_PoPMEmvN*CN-zA<8 z%i4_l?I#&pFh828%5&2owf9N@S2*G>p|yYE(K9Iwy$1FTKqLln<4|*SsgxfE1Ep%@ zg9^ft747n>EPVvCI2kbbQb;M~i!G3`CKsy{!tx6t%A2IxQ`Fe?BS*P3=3M?()!-#0 zh#+R!3wfW*x3NMHD<;?6$7sx~2L$8%G;7qDrpGG8gmCHGDUsHw^x|@Pxz}gDefb4$ z)Xz%i?lec5;j0{>@!KH!u23$#~X4%F_SzZepsvR8LYK&(64w$IW=D4~sWUrt9!wDp$ZMeC_;ZFwgCiPt z5`u{0dX5m_6t7)lc16`4koc^TuvKkpECar7c)7D>bZd>xSLBbIyv5x!g7`wkm(!9& zy+aUE=oRv$kO7Y8D&Ln5rYb=S0FY){-+7{b)(eI?$r!5b80vAl6g`)I4YN*7T`Lm; zuS+GthKiW(I-k*8KC4jHW?vb z4z-sIu|k>(PZc8CPeWe3FLPl?3NYgZf~3Qo3UEUdbOrw!<@o+XA!l=?(|d#kal#-P z=leF8aV~X^l#%_b(!MJTdNZ9eXFJN^!~7X|Uwd0PuS zn+}UgW`E{z&>hQ#8O=W%Ec^cR8l*?ZwigaPij6Y@5{8LxptHw*z;Qtp$Z&wkbc~j? z9@YhtuGez1%vU3Gq?d%zT6a(N(>*?`(X=hfBMcc9fJ(JM7`oe)=%HGo{(3fRB@Hn$_XK#of}4A~42**Do0z>Ey)p0zB%W;=@-t3Jir( zlUGP6$7js;7(ajIO3TRckVeXP_1u*Ym*jJaTtL0OngdJk$cP2DG0vM978)FEXsN4cKobW=13*)jqiXm+}EK~F`r}qRr%a9bo#wh;=wX;FT{$x{yw_HM|Vsw ziSXx%!D1VZ>UDDRdg>|pD+1*J{T!Soo5;%U+aUBBA#W8!H&BGOoQBAhNsM$Qy(ui?GpRG zS9E>J*Vk7?JF-37X0mo>Y-Gi2aF2gwZgJ$&GiRq(i12fce7?5){p%}7BkssKp042q zb_^24Jbs|gqZ@%92gRD9Gg!E# zGFybhL9;(qXsk@zz-HCW30PnzjU(^sv|*T30M%#>33a`swpIyY-n)j6rU|_rKoui| ziAia5?#x1Bc5n5uqf4$AVIICzG3_<-O*&Dsf}?@q?Yu9c!0cIS-t)@CqpC*{1Hcg1A;%MX)^)PS?Ucc}*?fRoCyc_Bvs zA7<}8rGqxHFwQT?WEGt%Do4aKA@Tan2;9(-Q}1%l0|L6p^PLyU1|5QepU>x$QMt%n zs$yD2)_?uufB&)UBc_JGAc|E)154)nnkZN(`Il@kXp)-+|81#+&N(%gy!Tt4-?z{P z3@9=x2z(T^VEahvNgm`4i_(V`w?wkV{6KBS@@8e$}ro!B$})CHa5mc zfAutIeW_I2{PaGHWMx?>_6hK#LdL8*q6jV@+n7a^$Q*?gba=+}7BtQa!;zQBJ3X;{ z6!@HFV9je5U*^2tf6TEO)pRnAip)sBvyi_>#vzB!%!YWS-tlwu%06LBZu9D`<#gbn zFQmudAidOh=rkyenn`#3{?=NU6Gy!qI=^IeFcq*ffQjz6@bd(GYW+RkV)bnN2SW_L z7b5%{kwO*Usu7PfplKx8^&_T5+|7$A;Pjr|r8knkI!6cYLx=iKMv~u~F1)5+Wafic z%mFR~)6pTmX~j~MvV_-B2vn@Vrs#($oZk|i!FkXqYht&doS(|@P&4|6mg zx_^HEiDgToJMQzyUY56-yL_MTLL7O;pA9^6TB>|l>WJO1*BO4+#A1Ymq zJfOnqKtjmseeER>XE!?vISl0$U}sZ#)Dz!>tqZ0o!Cpc})yM8DhVR927**Cdet!PF48gLsWba>q7iP{1 zmz5V60(TeunM7IzxC&oGC%6n$jh8>F&Lx3fe|Q1v=Ry~=tE~H z-z~d}H7?_C4!6tLN3WO2_Zx+U;uJ{U*2XZFc1PfZgKN_Q?m`s2)g&UY=|>-og9 zc+Vs2AOHBTKTMZcsccY9NxbA{9<8Xz+b{p38x8|sG;J1>&Er?ZL!NWx_n*J(A<;2A z&Ubyrp2~P(dzPw$?^IZeh!=u`j_6WS5pT>FGGgQwTfJzZ(UsQROQ^is<2fJIs$Ba} zB1~~FtMSm>3%shLA6egQy*c|WiSD2Vn{}jT`1?PftWpLyv16fX&CB-Dm6objaL>e~ zOgS+t62_fUg_AxQFuX5f5L zCNdles_&(HeHXUsowm_2^xyr?D&fLt*{YGBj^C$thm`vA>?ld_k6adx&tBQ@?I3S9 z7WNLf$hE@H-cJ9J8Bk^g0FAIh&*Xt^FNGUBob>Cga_dj*IzP)*1OpkyzE&Exu*p#L z!8E)V%-;aGGK3AE%Pp}_pupKBi#K@gR7CD`GPQK=%0F5E{m=jYgNNn8qMRd*50o&0 z%Y$tCc2G;@d*n|-c_40mbTZF#_^1MK>JQWT3q1}?C&dl_x znmD48PDG=uF}w{p6Or92k&a(2Qtuo45Vu~)J3D*@2`xNv9D3-s*X!%;#E38&EC}CacEbXUCgI~ zkNC)f=cOC%gYkHefH)3p`oddGpo7qB=z4zUsc?kD&q0tZ$jhJOuh*e(T`(~kIy)+B zU#-X3V+Fw_;{utI;T|o&E-94}AQrQhm>S_KDr!kSLdY0<;UM#Nz?wE~Ky`$BA+k0) zE*ht z^+{hnTds-0&zD79%s!2L_c!LczMd`kPob+VZ8Qhzmm&$NBL==Hc(trFU1GMY!{Q;o*QyZ)f7Z|5Fe9#4!Xwo% z)2Fkm>Ird&h8_Swsqv8vsb;hiZUPL~9?Y=bJA}?rUsrHbYW&(-8GqL`I-M+*h=iCs zjWq5-n%-wosoAJ(l#wgKksys@y0j4D8o)zl{-36%6zZNz2H!g_R>SJc?d1e_tJ)Y5U#uO$OkelLqCIDC_9F zP}w75IWMwd(nFU@Kxr}M=sAg0ZN-3Y1|-AT!E&^nK(ZvNcg&jUZ*stZt@J4vHxN9EVorz;TTgDf+7o>QPTnrHkT3D0GVmBTa- z-3&lMNR4gz*p?4I9D{tY#GDlP;~8)ZV|NrCyi_9ZRt6*y)AuN!nz zj?pgNy&M@~)X_+IzObnvx#wa=9WTxi4N!FS=J|q)7c$BmNi_z=F^fKaK45bww{u-f$)xYGpyYTxNelw@mDhJIK+!AwOg1`5sdPh=ZJ#XWn6Uw4+OkhR zqduMZT11ERrbvMt=}xBFhysDiUE#&64bfzp;e;Xby)o_RRQG@}&;&PB?H;JA<7m=| zaY3g_VuUgaJ!XiSU?<=siE-dVSOAT&YmK&<*FzCPNnzs$-)m&aFTozQN6adtTWMii zCbSAwRCp)iWbrKvq~6Y57YVukRD3Us2{Nrnz?Cs^pe><<7j>RQBTLvxD_Mcf?UEQH z{I+W5L;)Sfs`oL9S(ul8o_hzM)4BO(ow#!GmR&!xROCv?7LUxhm4%50X^;GJt@5b% z(!r(~b9giH`_vCliCF~?F;Ajgn!T9hXo)0Un^ipC6?45h%FI}J&$L&N=I@N3Pl zd#oelA?>+3)gI}#ANkeMoFtAac<5#L&h2~$?oh7Rf_K}J(Axx1GpNZTqg(YI^{HwS z)+u#MOnW&XqGo>!FYR2BtzK8nk>9oM~U zy0;ld9*OfQDQN_76Yr7D#Pw^5LH#|dLMYF<%oUf;eg}h4IjeJSX`Q ztGpd5$d<&-zboIBJa<3)1E4JG9M9408dC%AlrEyD&%LtUMLz3GzV9OfeiKm^{9+ue zB7wAP?R0XU^9?)!##I@$1nyOI=P(vqmu=;o@xufYvI;H2fyc64(W0G%#|mndccndZ z-ZGWTY`tHoZv-GV(Dh%Um3pJgahW@@Kh4 zPOX35wFCz*iovw%js0US^||-N`zd25q?P9lED^sKXK>d5e2(uTuGe^!VNqQA&JiL_ zwV8d<%V|{^1&CL;l!T0Y^+C0TAEkbNT{X&~B8xM%QA9?9~Dc zE_2D64dH4v&0x`*JQ0V1QF)%sJ*0M4x~|SPPgo_-PA^k8nN}C{iUS222@*7!YIJhp zUKvBq0I$8QcFxsx_@OsGKK40?tN%dLaBK63y!4C@JueCFER-D9$RV*tea+z(! z$mS30@qikNU*z;$j3$(iiY#47FGP}?Gjj4G#qSP|72-lM9?HOYXXjQH)Z&$%NDraeGW!U`*pC@;UvAjcDWGN7QYZ)S;f+OcHjK3HpW(0R2k$}drcYGoBVz0 z&L&!Bm53(XB^KV_6C#H5QA{LG15IOe1FJj7QfIPg;1nRTu*&zRGeVNs9TKjXs4S5< ztx6kdgIUOrA_8ynDfi*X34tP!VO?fR&Utd&XLvqg1b-eEzb9kB5dqE}9mj+oE?d)2 zqFcn&LVlm8Dv#?f&wVm3_FP$(Bb(3O)u!#whl$VPapY@f0)qRKH?XC{(M9C=8Cr6a zz=K6nHq!}n4Ae8iJWLOOhr=7e@rSiJC?D<&ndf4PP_PYpCC!DkG{nB)-qSI#q_m=s zo3u@bTsaDVJ$@%t|G9K+m6L}fg)s)2+ush>8i`p0%=kiQSBm7~9-)ONe``wls7|UY zfqCztuU0>E?(uV?Z)Xq>M+3?9xg84R2-qG{#a53c|Hs3}i*EJrXxPYU;q1EqZ}Plm z$3YOC!VN3~JMA|QmCH^9g-p0e+*DH)d&$AIl!W-*lQa7W5>heJk&ZoX0xk0*a<0uP z(Onwbp;&l?6}q4F=t`{<8!@=#r*V0%82Msxi3>EYmf@?Y$=D+V{@s*Xz7<3<#yCZ2 zjklAVSuHIB$z`k!!&sShO%P~k4W?mMv79MB)PehCjB+Ns6vUVG(0Tj0Ykhj#XFmXhsXp!j#0?y8~(qR zXY;eet3_2F%cf<*MO&PeC%v8pqoExYg+%%=Cj4)Bz`F^;F#VX8GX3NZx zWS%ExTlW6SGf=7AVuAh14hR=Zp4~1xTlgrdD-*2F<23@=w&eSss*rQsV5QW^O}&-5 zCy+y2SrlSoy0b#Yf{CeGfwqb*Xg&lOVbz7T1>9kkiwZ3aVVZsd_u)z9K!xTvf1$&o7k>AuU*1=BKP0Q;CEeC$8i| zrl~bUl_5(E&s+l^GS@OxjXjS0{SmZ=d=+H2D3;LEG{tC79!iZ~(NUFmg(&u3a-8fjIa%St& z)mG-jd-@=zOV+|JXszsa1}0@*8V$w_*{juo`Pr_%P^JIwSCp+Y!gtj%!LCGg0eH*& zms>L39ub2NCO!lVw{m4f1fB6t<&uu1OYRi0_-!UsK!T5Ie(jOUnV(OM@o3&0pM4%a z@JUFBdP+mXb9{8gQGGi|+ff}5+z_QQ8%F-bbI6P^$am{)B9(W1G1Qd+VL+b0DrNdZlDs%vpSIS2taCo)S_ z_B~fVdy6A{CGeuV6BT_bw^zs9(N-pCnQ)=}h)SZ3C9uR*lP6NKC4+h^njMQmzrOd9 z-{0T8L%{O(;$kt|c`*d{9hTe15a6T%Qn}D=ME>x#NU_B9gYNdTr@5M&r^2#q~hk~?R1XroN&}>-$vp?odh9DeDDGLZjLiBhj`LgCsJQ9X$3z&Y&ks7s(Q9it^2=_3^Nc(=n#`?yUR|&p&&iM?Lu9Q_UIM}QKr0L?3DgIZ z5KMAJxUD>FvYT>mZ<6ODY(s)fePWT~zur5|eq@uQ3Zg8-ZFxRbC6+w$e&1sZ!}M$|G`v9ak{E%g`MFS)i0gbE@u8FfHs?;1N-4vy)opA;6cQ!kx~l9aAuk z61mKkS3x~{1!XnvBrlUFM{Rklb>XwMA{ea7aET%p_zfJkFegUi+7~L)padMxlCD;E zv5>G((Us;Wb*boG6%pIHLs(cY3ciOLSuCkBk(M_mie{P;V$gy2D!-DI1pSt0!`7!S z+?Us23IGNVYoKxX_uRSsM_+r3oT{=cKjZTG1eV*jk%3Oqs*K&7j^ZT~MjftVs+VdZ zTy$du%Y~M~?&SnG>jiSmJx6zKc^T=CT`1pIf6NPt~Jw(!hUxbuNmNfBN#>P%Oe zGDAZ4UcWLJ%5J9Utu4>^wuY5nTrqG+QS`P?oQdrc~0rDRK@|#Ur7w;+yaYMg0+Io!6CDDxDOL#jeD#Y z(*)6(HgLTTF!Ih|t}L{I$qRq4iY$gf=%pus%DOf~d5z4LX;sEW0FKofe8VqGdMa}; z{9=ahFX^lM|4-JtJ!y7aSAJ{n%=1-MSKk{AuyG|wf&xWK6h+Nw#u4L*uwVRV&Hv&T zhr{-~I2;bg*x~Vv9h62QMQSLD!T_ibPdM}M1_nw~00kli3wCUN{2D8`st^LH5J zm}P1sRZgw%s9G0;>6+V(1O&#jnu}|Gjzx)AFC<)d%P8Uad@a9*rHs6!!cWss4t%jAxl)eW%V*E@+pNJ z^8<>xuucbasWoM*X2VIsVonS4Br5WxA_$w z@F>TzVmIOvvMJN^!*49bQ{zYArR&*~erQ!%BQIUWg?pCB*S0?-DY2_it?;@aXuuuT zm0f|I?2l5Vr_ncK42z%Cj#6T zdX~|kGpI%quhF|rx@jtcyyNJ(30?qlHtDJ-swhh5REql}%u+r&^FAcA5!?VaKf9@% zDKp2Ks<<2x5*txi(Y4;DAufo%xg;+q#9ye76O&||q6jy8I^w_&S1p>0_)@B8OS1#X zRJ(vHE)lxZSjx_0={CG*ROS;*4~St+*RyoTCj^w0^FI2-k$0N)!r;e*#Wt2qdsxo= zB_-58VyH{fpyMG1%rtj=%I5|l@=CBDcXBcQSPwLZ9dXn!gP|hxp;PdI;eMQ|^G!JN# zob`?149QGO6IRqV=1kQV^%z!chsbG~naRl>Bx7!4w*dvTPlzJFeNURB>MNGa%qNa~ zg~4%GZ;k*NX3!x^?B?9P^fBCt$}WOV4XQMPQrnlqK~Cnyp-##yjm6kCvz}!-%P|kA zlMzco3+IA6%w2|YYMNYVtP^4Gv(xaF#F!&c`R?)H@2L-TbaI)(Pg1zR?EKkqf;G30 zUrNZdITE5YIEV?Wp)YrHF{&EVFc#`Uv02sIYMS#f@$3xUY&|b(p>%ghNNP{O>s%kTuRR**cEO`M#ynOH!Dk3409xlcP+$ld5GSAv_{pqX-*}yB%p4Fy7Y6 zQMJePMndWwSFV??7kxd8G>tPK)34P>PqkDSWo-amETe3LykXY;q8XA1db0sz*NG(> z0Zd8M_*_M~|DcJvBROGJr(h%Ecl*NM6Y*Zf|Llfl9n}oo2F_%g?@n(nBr%|wp_>b3 z4(e*#y)_(arL4JQz$Xg=m;45@rnQJbuc*;kU0v@X2v9X0%cWHGzAwn=Q=Zf4#*Bz*vWdlfy1t{Uq0hv`0b;H=^24*BVqo`C3T3CK z?JkzTTc9}zR>Y^o6N>J182Ac zYRzYsgHS=G(b6;N*-rO0;)<#;sz@e#haMlkM60_#{5tu+T`=ugJud#1fDmSmI9C6L zA87H}t{D(fFJ{&aKg*U$k_?6l1=o_xo>9bUV6ATv z(wQGdPI>~O>K+3#r6Emk&@}$sbNjV3x7fcTWKzT zMWRcfQc?;>J(QRaKy{m_9Fe^2@-k#rjAiHKpIyWuBRNeaE8k?r#lgt&VR)wutA_$J z#V^q?OW<0feP-BL3=~xOa~b%((a0(z_ddcxRK(FqFwH%MzbgU1By;f$U)2mg(*mQAr zo}-Z}e%L8d>#YqsRXM&PdPG%GWJ7BWwfMYz2@K$O~o)pYXvh)4#~^vv)ELEkkNj z!pxsz9UTQaCaS{hxXhMZ5KVXUvfEDT@EUm=JRt~9z?BK|3)*PW9Cz5V+geEg`8nu< zOi?V~!(4KwU&m}=ZOxGlglLCSCy#2$%U1D&+s8k}apueClb#Wd^h}~cpC}s(4pktG zZN?~19@|8Jts9wWs8m2Cnxae8@& z03$l3@M=iXYV9O_P?jY_(Rt}ivm);i(wA3`Tu9eLP8_(FjJ7@5XAX=}7|r0_gD(35 ztL}#igQZDOiK(W8_(3jq!r8+TOP~=S+t+>DScnIfvi~SxVm}Z#m?#S3LUCcoB7_K) zrVU{D;jRSRS_H6}ofsffWU*QX7S&j6@*|eE_qg=jv*lftlG|tbegVTfp59Tnk`m*- z_j#@?`^74e!b)1}p>F77hYiEte3n-^(;ie*puop5-N}%h9fGrHESHf(I@;c;*^U=H z=94}{Q%s{D7JPfUPSwcD8cQHfj14eb$Hi&d#|~piPcCtVLGqcN5jo@KB;;ut3FwqSx(SfW=1@2$CwqPsXQ z1->PQX%LejBXGd3rJO((LDN|7+Bkxglz~^?qa2-yErDb<`O>i~8g9S^ks9$H`7=?c zcL3otSOg*w5zJ?})a^tRlBkk0<7tDapi3g8LLnuxzmb@BS1`GEK$p%>*QCR-U`hC( zm$?YS>CjP9Sy&e0^3vw`g(fv6^I|xeULYLo9DSLQM->s<1g^GrMIXroXC{?-@B*Yb z#r+c`MOoz1FZndK9`%}nfx)rW#+{-2PFz&7Ri_$w2ZgzRqNt%I7*gFh$OGR4=RA(P zkiH>`+|Smku`Tdo#MH-_JPVHdj)PXz_zc^?sNG+y;hvFFD+WLJ9toq71JwE?Uhd<`!dtsIDe$;b&Q^Ec49Y3ro}O6u03_$txuzq&neq#RG%qeU0mTJ zmQpt?NE15qmQhFdnK&~*tWO8BER;@*PRS`;j zn3h>8hN+?x%xnb7@j=aHv&gKDV#g!F2O4vf6(wrqn}?xmRbDyEsVvaOQy62Znx?e8 zDC6fXg&wJih*vErg}wls_)GI*$Sz9e=U0}f=lsu=s;XQRmxZ<~sPdKZ?MV)|d`0y$ zmdiK}`$;O|;-iZg3k7EmBNc*X#z}@PKU0V}mN7LeWE-Sd2jN#Nkt;Tw z2pbnp!~sOdR0yIBAZ%2}rG)v8@ynylNNqm#Z5+|A9$}2;b^=T>jz-}s3ypfo2C_s5 zO`l1`(c%v#2Q9~@j=qoV4_5(B|JA^d#nO;*%aVU*?xJKCy@Nly7Rk}nq;HV^v7SMk zycG2jXjNBnjv6~RMeC#i#8e^CLW=#kW7`UG=qLmcW5_(-{lG6aP>(lQEjC_=857b7SkwF`B_FW)uC8)sVV-etHC!J%xpOA{0l9 zv`!|6XgR`sK_4>Fh{80aUOq#EY!I|3CwU%92;l0@lA?1ICChw$OkYSs8dHYLWQU2^y;nm=(iJnHq@`7pmF&FOfZWIQ##f5GuAPAM(9C?BZ z*CaKo0Amt(1L#?vEeT-;}>WaT+v5yVv}@Ueuwx|7?#Rw0Ha{1T2Bk%>I9n1TdxyLoZIwZR0VVzTf! z5(`STD-1)_HE3)M=Xu7fEWjt_hX4|OvoYK?>)vvYUx^^FIt(csmx50k*R{32Wsf2 zJVsUi&;&JNl&~tignS;&Yz$kL`(sHlw8f0*xe0^vJkCrxqSIWcVlf6%krl=$XfEXv zwT@+MA(Do!AmJu(Ql|1Jja?$koYzw*JC}WysSmM5iHh}#yXN_)p#r8+aI zC{zv@29!d7Np*>BDF$ItCPkOvuHk=wSc>3(-(fpXYBq2>x(d^*QBhaR=EmAFUF@Mg z3$M>0-B`4Y9nECXxyxAm7Au;D1c+Ku`T?yyogJyn2!}zM zcpobh%g|eMT5XDRi~(fxERZw~+gC(6WS#La4)0f|b3IjonbShGXYZ-}i$u{%8Og% z59d@+(S)Uf)`nvYq*7&MSp~b3J+zhuS}N2Ax(tS(EY51kvM3F?XW+!!Ow|MDwo96x zqXELDK4qiodPgb{N?vqs>!0e9oRhL8bh|%Wx-pGHHV3RkHgv4$Pg(drz5w1(>)8wn zihq@bkHuEMgKo~uQlDTzRdvEDAMO~h#RF9r)OjIqst@GiIEbn9f4?yNOU>DPR@JOf zQ_>jREES)9s!sJ;xL$*QmLn)(6s_cVFXr0A=t5^e%|-=Br`pAZ1<~b2N?%7|T=K|S zx+_RR{+ZO-*o*LR(-Q(Oh}KwgH*%)liTs~{!R!{Pidw6{L_rKd%jS!F`YNg_j{EM_ zEyWWMMP2+oP|c*oS~n^jr-?5T@{r7;jmd?_4dJ+M8qR9wF1Y2kg185rmM&e+U zJ^)W+JEZnrber_ynCGho25Lef!z4JC?1?m1Ds0KQS4v{N)6(So2mywbTpiFa<1oXL zU`JSw?;4#N^ipI&xajizTAH91{vb&KNMopvHA+e%hfFn@FrUUVX%@M#giLfy5e2;u zdpBr3aD3QU`IUU0oX*gpWn=Oi4Utt-T3u2;8NQZ=hPtQNE9igY679Qm~NV6rc@yP&D=~lHIn4twoe`CqJyG7e)!GJloFLiNs~O zCkAKOo?V7nq>iouXE;R~$sFzFEkX&ln7tTiE(rp z7YFkzilot7F9J-J+s z>0f;;i0xb&M;Ejh^O7n$r};J-kXmlzOEc9rKBnv0aTJ{+mWW}+lCeA`rZN-t*jf3Z z7_M$lJv0PEzh>8yF{fBYFKKp*P^d5!^%KN65b{Qs4bNRtwCUBy!u*}}0lwhofl{g% zDj02`)&sxi5b-UMu+i`{Kq!E5UKm(v|1(Det{C%m(PG1ncw7+=S&NELX7?lX#EgY# zpaeV);$T8LeUhMOQ`$WwjV1|%#43>BFSYjoSL$+r%2Y0itN&m@UA0_*`hq+eoGeKS z8`wH!x|Bltcxq!oQ~7nKbk%)N^j*lZg$nK*fKkll#(g+(l|WJ>lg?$=Ue(%&qKfnf zVLU*LURaF8@4K+tG{%}nu>z&zengxD-P8yli1Q|dx89~obDjIaTHD#ha4#{YjJzxv z;ZimhkRv-s)q+D|WVXh8Q5mdcspL$q)e<5VL;+!_S8fAkV04a9wM>cw$Hr;Y6wfH~ zosKMk)G6V0U(R6R+VXphin&wf8%*7a-;q?~`G`+36wvTnEs82cvC?NyTQKCaQRg zT#s;USMDn+_P%?&I|gbwqy()?_ro_>NESE||B$w-C>rAfzf{`Zri)qJj_*X%VC_Vk z#w8#)8w_0aVz!COHVK#Nl|<`}v+U|JG@9Gu0+i6xbCjc0?8u65U_SitHiqs05>6qZ zWsk}y^emv=TEd6QyUN5k`(r{cSBkVIWUm$jK#=9mjO;w>_8psu2}ha|8hz zWFwi)n5d{^rg?!c2_&4x$->irWTzAtRDc&JiC}Pt6SSAk?biS1r7_-q8~0LV4Q_xF7plnYTQYJ($U!6cz~CHl1}v^DB+4e2Go(M(?~(LqV0yE`KMc=V>77pT_Sy zM&7G3PjwQqLdiEs*a&Xd>J7@>8#8Nq_pw`L*jOooXkq(l}<~3W5f_wEihH=ZKOnQH5r+Ba8z$CGc{x|b&Q;PVK9BP?Ot8m zl&i62wlI>K?59ZIZOxCL}a`I@e9JmR?3tK?Dytq=b9lHf{6;bOMtg%xQo#-F_}fVonpAub%a zF`_f7?OL-OqXe+B=pi{ly0VIBYSSe%WH8My39#YUSUSgGb7|4MTt(?u5Ju8U$t>zV zm~$zZsxCtoI3Xm#GIm55!d7D-$1H(mu|%*Cng12)?~EW06QrT4FzhX*CxC`o&pXGG zg#&-iV1iM49uTV)c4^jAFyJ_%mWP=!14AMjHPSFea3s-ts|UYu#l<*IyB3VrW~WHq z)967YF6m#1d>HYn6SX>EbEK>}JP_c_UKentQTU@gqk>46O$`p1(QCVad(#Xgp{Q7vKrQXEU!Tq_Obhbp%Mol(%qyN2r~Bs zOJ1XJCn*bP%)tFklr|;E6$5<{%mm?L4FK~WXc|m=ZyrDx^y{hj?t!C+;Q+>ibeCvO$^mm?>+U- zwmP_TvAQ6kF-?D=dPsbLxHB~n<#G7_0d{T4nLxJN$_2B`T~H!6;=w61k*$wOv|W@; zRnPnYR;BD06MLW$c12}LVftREYm2fH7n(Wsl#I$3Z>t;CXmdmqW4S@qN*}CZxz{MA zG9W54t<@9m_&qsInG06_jFw}ylF&&p&ku2Vqe5l#Dx3Mo*s><+;eyTO&nwyHZOA+>)*7G9-_?-_06Dvq?QU5!ExWc#kMi$#{)PXckIV~q7>4& zh6R~ykTuAU3Y^|;jy)FFbR3c{hdgCoIb4qp+Q`vZqHSXWwO&kxiD9d0Ye~Bz4uT11 z89M9O0Vx%#hvY$pF{*fpS zS~nijQv#kI4=&S%lJ(K&&*=)sb`lp5vS?XI2m`WwJy`@4mgA#o+?(E~_-9>4anXg_ zm`QiS`SysMZ)k?8LEK1OMALg@zzHnT1l++my|!v0!^%7SRg!8UMV$Rd^?E`nub4HcB3LQmN*K^5VwR(97`9R@?#2I zw!@ZdgTq1`XuW&4O734R4z4vFX##i8nWRXnl!C2PHWvOq?|qLBX@m8~S!T=D*%h!3 z-m=ksiD$!|t}1CX66QhdWXgxZrbcQR*(-xiW78 zzORTJ^}#Wm7FjY&Fyn${b_ua?A4%`Eyclb(se%h?u$gk6E%})b3$t2d5fBSGp$E&d z)60;;J}`=A2Fz%kOTO5x_%hQg)Cw(vt7G%dRRY<~s#NB@Hy-*J2TF2#W&sfxlgy9M z;&Wt-jP{hQ3YIV~9)9jM@;yrYcTf*@8pe2CrU~jYQ-0ML1yqZVV$Ti-9FKdFe&eud z)@dXo%dWfh*WagL6;ru43|l>ewLML|h#VZ@;VgnG8ye4m*4nbvW%M*ncSV-~sZhwp zG8hDk_Gz#UrId?wIC|nD8)#S~JdEOpOsNcy(qhgWW1!ZJvR286U(YvjMN#;5dZI{V zsym~@UIMaw+Cf5giBY2OHr9x1S99?l9F}9g$dvKmCiCw}<_KFwa_p)cGDXT~_|YOd z!6GwBXK+-ckq*kSV_XXrn0iM!!pp|JQ4}blM2sKi5)~9uss+W#-Sn3jvqj3Y z&pr2V5)9}-c+|47+Vcp*cn}n>I}dnbN+<)*F{m`^tPsRs<}nP9pFH*3kBlfc+Qq^~ zX7J2F&fud;4&_RWt712jDQ#KtNwRKiOzuxCkg()%q=5{p`+2Eq**ye()_{}X8?ShL z6jSQ~MYrX0nR(KMRafq8WXAqhet(VX2~foqCaH+C^h#$MA*02#;p3HP>wTH8nG9Fb zA3~@hE8@6NEa&68m<0)QW5*F4QutL$j2tsWjJfxXt#IA)bIe#!0o7>JY`Ko+a?BWh z2ueQ_BZ4Ua2youiD8zeiTZONCfiU`6PeuaNw`%8_k&so32~*|@aln2VLP^0Ua#j2)nJo+$Xx zk~=%Uq^j>~_`LU21z~YVM>=EY=tvl0YS?N$d8azP8c9%&bWmtYbHRPgboBehyUJHf zAmwFp%S7iWEh^*49?t|sj(R&i8NOF-dCA0kAA*vig^DApn0mu;e~PwWdG`@Uxq_3J zgrx1HTX#jq%<-9zglfK5KC(g@TIbGjft1?J8JUsbey}IqfS*^X1rj0UG{%~<8$uMkt^bclPCfxS5*{OOPV50^pSLII>A;GK`%-5W@m0 z7g3OLnME}j2Uyi|;u8rxdL9rtHqyKZqk@3Bgc1wgSEyEsK+|-X#>fX_iB9DuH$Z_x zq$T>IGrR*nrd#Hl%d&s;4y`p|a8sG2$<&!nTO{`;@cVAqquHp>OoNb$A`uG%ThU4K ziQJDE9}y)}cwi-@t+hm`_cpQ;Erl*v5wyMKvRVL?t>hu<*%`^>@^7aM21& zO~0dcf}YTAnRT0cpG+hV!I4}q9>&4P3b=gTd|n+AZA-p!D)ur8IEo{<=fu1L>zvmF<-Ik`Kq zCMA&am|a>UX*r5hDw*6|H%@YJ;__;vrMWWFMmxKJfEKmhu$6)ma@L|a)QY35U?WLI zCzePA;SN(5wpS@`V8$?L^>fJCfi}m6)-&HHkos9bFI4ZDlG)eH#8f<*4UDdtn5Yq# zjgl97QKC?wsmvU_ZLN(o7^`?O*t$=Ewo;wITPlvtaaD?-Bu8OM1)+MeQih<443w&b z8B#jNMYiJ{{6lw$Sw|m7zRZQ*1daiCaJf#Nvp!XfQebvPNn9iPd{%Qd6YUv-peHzG z1oKB?c8<&!89>S_V+7)=qgfe=g^yygnB2GWmDtS%%`Bn*S@=7SiHMGf2#SOk+Kjsl zd8-uefGi)5K}@`a>mh?CzKT8UlZ~Cb+(?oBmxDhQR(=M*4QzDBQSmlN#uL_0Bz`7kyUKS6gvK;el z_28FD33{kK>S)f5~3NQ+lQ(N&RyY)ed zsHLp6^XmrcMFJYgg|!&RMzOa7>B(ckgzMbDnIK0gM!|=OcX#3Ani(ho9`(y{Zyg8S zGDu15hS6AfDTxw`B-Wp&DN`+?=(a<&W|oD-bIO1-KME9^INjNEUyy2~@MU@3InwLZ zd3Y4Y21K_t*lh{Hj!|SPbV>BI6fms3_Z)qSW9mCSZxc_?#WP2#MhCu8(BbFppoXfZ zb37SuQ6R@uf!;Och1-~a)VZ&*-keN9sz?;nl?qU&3NU{O%1H5oHvC&V=Wr1SR(6?xy zldluf*qk3s8Y&iOm}iMd!u)DEWX!ARZqiX`ypQ2{3COlPig zxPO<0_pqqQm&+^gACXHOp+=qh^|*iwmP~C~kmCNC7V7K+m+xxKfuU~liacl2Nzx7W z(x4J8L6qyy$OF?mBkvd*FG6TRXm#;~#OG(x=>+hPm;FwX zYL6MM%^U`k0WCJi3)Gv7OkI2)fFVoe%jmclb`QL$%nZlgaVV~C+5-R@@uwVv_Ay{8 zClqPn?oF0hfiODe_Yz%eu~Sb+7%tiAma=&#Jd81{ba;zk6v+ZBzD8fzYq1cIOVHrI zuc8_;d9H@!rnz%bU8H0LLK{9}a^QEEoq=#R z7~#NRX812Qy6PGCeYD z2-!`C*TN$gM#{JsIBj@Q7V>ifTnSli*5_E3)j%%b6=}rAlE^5ai3#4d6p5^$j?r5R zXFQg>xw0$bHbPY6<3zeD2M01?gXg^61~U%)r1cxJ=%Q8uDZK`5V8dSQ;Tgf6G6Q$-7@6 zdqTBN=5*VfyC;I1AjeSu#DOtP=HxRz$AJCu0Zk-Y7*;54?EEv$x+e^)xAUFBlR;z6 zJd~ieLOhRxQc$0f*BR6UO%kara?NOYL+vHu3E+$BYAb)ZBCC8bQ8MTHYxqyqT*?-$ zvFU?R`iTt2iY`I31F=%vty^58D=2x$37yu1&V6IM?&`XNF3WMi*?P$^F3hY476|T8 zH`F?DIAKtYP7Q(Zp&hJHWu>LgIO8HY_L*u#mqM1qmN_y0VmB_d$LwO0Kj6a}{8hRRs@?xQV;dd-nlqJ3;& z?+u-?&BSA6k?W|P062~r8do0Mq2hG7cEG`(5`i;Y0T$XCMC zdMdvLk?FEX=QaObGW;q$Lu-O+*b71=;H2Q@{4c5ugS;v_|F?}7?cNDh)PlVkyEm6J z7nMnhh(hvE4$c}H=7v1-jMC&K-f3*9NI}LtDrzpgC5>l2VOyE&(ZdY2EQ8A}nc^q_ z03ZNKL_t)P49@u6mAf<*5RwkmIX>}U@RS~p;T@A^M975b#X%6i;>hvXitl}dRq~Ya zlHlL{VVa2Yb>uZKDnQX9E|ja;!Gs`>#Rs7|1^g_+Vf2sLf@To);6q zx#BwhK)Mhu5l-&bj7+6?35sW)qy(A&9)_YDN2cyE2o_V7LWK+{{D?}H(qlsM!r#Qk zJN-6AE4D%rL~-GOmP3XN1@fa0gOtKKd~}LlXbkhqt(1yxP6+6T@t3=XC*V(UJRY+G z_O6S5h!`){ipB+fB)~?HSEABIme|-rBc7!EHb$>K@Fc44rxC{nnZSJ(yr71Km6Q@{ z^=H7n6P5c7@h~xVEwTTzQ0ZZD>zlfGfkjY|CVa%#a-S9NXcT?D_wG~WeXpv`ka)X5n=Uc=MKgSdSAQb5Y$@yKaYjAvJ&!3gu+GzuDq%Yr!u zR&uZsCnZEf@d7*vmZQvCQL$-sc8tj7F%$ba`@ldF@vc(N4*tlux7LR96;N5Xpszx>V2Toh~qan!HLtvnH5y% ztvhe#eRQ7vl~_O1OOaeQ96=kQ6;*HPp-( zWG0@$+(&1ct^wBFRAIsZ32)}bDVvD+HM(o1m zG`qrhVe}Vy%*vLRZn*ger^O+x-~u(<{MZ#R z5DhRiB2pS1&3()csFWGFQjSJ8( zL?I~BB5cVfYt6Z51*K|^Rg8}N$bAU|r!W;!3YWfNnw2T`2wD7|hpDT(KnQ0a4HXsL zTEZP;-mee>_XHLGzIet+$#5m#!#PCN7I`{UFxn1S%~8#C?iV_rrx2!~!q9zhWyHql zOrVSqsqFaUcgYVR@Z_#o8 zv*9L%bc7L5`Gx5P2-U08vxN-If&g8~yRv169Qh=C+8c=g$ldD#a^*hTI6@85^`ZD4 zrD0~aCnbnTppb0Gd#l{D*4F0cy`$5qx}~ga=s>lCc5FDmICJ3Q!fco=mC$iZDPvScKFBbh1xX zSxWu_u*Z>;mg23g&FVSKjE=Kzz4 z@h6>!xbMsgi@8!9UuQhMjgzb# z$^V~Ycw$A&<`_$s34vkpUyUivHEZZ$3Z(xel!#+kzC)1{H1}O2$7?EU7@rqIC1n1P zWrRD(D)imm)o_HnQu*p8@`AnZK4Fl8<9>8XJAmp`$`}zT-I#8rzuKR2H{rjSq(1wZ6Gq&);ss-7W8FA0qWsJtkEt2}0FGt7P16mSm zEuFJA6%k`Wl}81i3>%{XvY#f+g`p?V(8+U}`xcd%^cN1;(Jf_qPA4tAMEPQnYN}FG z8amK@8OHI|xB&FK*Kq6WH}Tn{Pf-sC?9EVhL+^^Sw&CHw{}|(upfX>?`he=@JC>+0 zpYFc&s%7Sju%gXNz&CA)NeQ)!x2|gxs_zh_Nah8AraXb5caDg7*jC+g`PZzY*-R!Z zsu@1)(Lt7!O?fL~gyM&P{O)#+Lw$@vRTLpN z*1M~z0E`w1i*z~5Mpv#sJ7Gfg->Ia}U}ir^hNsodpQFm%SdrXb_AfR=7nr2nMe9771HE2JC~>1V^mY(sPLlBa^Kb^tR7kNO7f~0cdST+Il*n zdnO!G>VAuqx#k2xlI)<<-b)0=z**h=-7T?%2KIi1t$OxLYYh=C;Rm4ep&|%NNfq$@ zOv_yRT8dvp!!lLS4TQ5mEQ*uSH+c3GMM}Jpxnt}MD8E)3=C)EC9NXF=9}br}MWg~K zW&aiwZ(n()K{U=zDZA#~uAi2d+TlnJ*(pUTi|}UQKG~u?;TL zV&5@ow!IJRZID+rvj@fibSsD=&%FVjRc}%#yRF=RgwDUifZ-z!v^+#oK=1i z?m;$OUYuj}fn&S!9YPlrC`vi=5$o{1L^7A?cy2M!7>c4B>e(fV9=w>iy5f!!7*cTa z_9Z)C;PKwXq0^f^S=xPoZuaycA&5 zAUwRp_V-CWcDHqRq>H*ACzw@{lA3CKv857sxo#wIMxIb9h0_z^Wi>V4wqy+XsFgo2 z(dZF1ouP8#-F=FESWJ&(--dI1F6YA^rz|=;m4z{00u_{678T$kO^$I|gkt$QYDo_X zv#$t9$nuNF$9J1!c}`CsQgbOC7?hCM%Hqjb^RhD- zMm8!921ji?%r&yXC+cKyu4`B$-bNnq(RK7eH55Sb9_QPFKalFJE@%iAN3}wAq~Zsd zF|k_2Q@uDo1CtSBDmExO)lys%d#2dW8tSEgPW#~q*#^p)p)o*~frHm`TwGk>_N`m!`xQR;{Rci8B|*9n%X3x)ssP16I==%? z9zCK@=ztqH?&A3uUI1*z+i(5~SH~j&S0W7v>UNG>x9{PVmtVoBpM8q=-hBt?9d+Aq zc6JT-pS_R!&)!4Zcf9%LoA~m}kI{}#P^94E+9jU3{~Vrs;RT%Oz)$|>$LLSKa3va{ zhqrIv!86Z2z#sq7YxwBHkMMUt`w6~$_{7Ij^#BvawTp^7H*Vl+e~d3a{{+n*K?d;> z5F%KBJNI6|H^22dUV7yveEH?)`0F426`npl;_Unyo`3KFuf6^np1FSyAAkBK{>wl9 z1Kxe-4LtnfL!doEE#_l3Feq(s+{gC`IogSI%QlP8#k3iftO+He%=1tY9JUR6I}!jH zV_h#ox1u?rP;nWs+aCs_3|!*Aw@_mlyG2O}hL6qlgnlsJyFnF}EQc@gCl(y88KUIf zVmWbwH2&T5;Fi-UU+ak&s>;Y`c4>t^so;7=PD-lE((PX`BRMQB3@MK$G}eaAzAwFG z{@#urmr5r$&*jp^X+Aja$21i-3Z<$C3gJ*I_U5WQtDtI-#&H{{Vv;FRYFlBhcP`q#~#HmeHeF)XTA$#8+ZOBhoPpa&FO=#0DE5JR2N+!~(=@ z#>Ms-93M+?|04?{i)`mw2>XMQ^TQ-FN|1vcGkQ;dM2s0G|AtH2fY1ao!6HX}%yF3k zDlVmM!Y~@^8A79~FdgW&0~N4sht-OPjv)iP9bwfls+;3R(_E0c5?e4-Al5y6Y0RZ` zw?IoDGIu-Xsu2hzV=gBH-yh6B#e@8TQZ zc^&`qU;lGlJ$i(n|Kvyb`A`22W@q^JpL`$x`oH;0{N|lsm zpTVF0;4krmAATQSeD(?6`Sq_cjxHW)^xoWk=4<$~zxYeM_MLCyjlcgnKKl4WlrFe? z_a46c$A5wszwt6|+`7c^>5l8S?&5d9`!(ME)vs`T`WQEF-^LIA;-BHgS6{``&p*YF z|Mu?y*?hjQzHd3ZxQQ2E`37G3_UriQlOF-7sD}$&TwKTf=bp#cU;R36T)&QMhco=+ zKl~IAA3g-|nD3N7^ZVYh9nMg;b6i}%g|lln@ay;9!H|Nlz4!{g@vU#;+uwc_*Do*7 z25{}xJ$(Ju7x2!nU&D`o^dIovyZ?ma@gZcyq8P9qX3mI;jt;g?L_Lv55-`4FLk;A6 zaOhZ+#$L!niTrD~(NZlJq68Z;g^(0ylpr3j(r1pLOoCR-Ft%|d%G`Z2m4&L!ayWyCreo-9`o<=WwflD2=2g;ip{uGJlIdt9NM`{ ztg1eCm9@JIGiB^qJ9Bxq6d(oU)1+f`$@e*^8?j3jD3yTbAVn?M+tE=)uCIs5Ky~Xo zsHKnZUDMbVj^$WHS6!4%p~4#B3iZP%el^}=_EbU}l~~4`G)!n3`M5C_8$u(_$h-sp zfGVlTl80G0F6AWHRY&Tn7Ghrjp` zC>vgS;T!mqKmR^%KYI_C*DtXZ#qseoc=go>`1rGr@W1^p|0A9}dW4;>aVgdFbL(0Klrcy64#%(1wANm*dVn6a=_)i+qk@c3+2NzXx*r~Q$Q-98+1FzcI^^Z*6_2R z{M5S>J;zHgy@Kancp2aR?zeGy`y8Kr{1M*z$DiW8U%kb#h$Z5D4=|q`X{J($Z0Ox^ zY#mRYJjPFc_V;K*@!X3q;$QvO{|fi+-vzAUi$`B#J5>DHzxZ=Jdh$B{=KubG@DD%# zdt43Rvqw+xf-26=&Tyz39@#`*KnnD5z}OV;fBXsF`qi&d&o1%DufKxte*ceg{l;C~ zxqk<$f~zkc;l1C!4XezhQSV_@3s5RbIm6-n61VQ$!P6&4{Pw-yapRQ|-|G|w&F`O|PjqrN!5o3l z+51fUDB>cG2ucezm1O#+PT>Xs6Q9ypP&zPjOa=w6+LeD#eHx;>b$Wgr;WX5sK^PW+ zRKFh-K=lV=1H{cax}cu>=sUI-{vr9a_dW5&%{Vkve+`0cXJb3v9w9)a#FMf$nKKvMWpM4hJd;Ob`%LAT!@fE!H z_M5=ous4I4rzD;ouH)W=7jWm^Z5+-k9zIl{c;{6%#na=CPai(QRX?J^3y~s~87gPE zefwE_?S<#@$!8zqgWvuZhw}^k+4uh${`8;yId0v)1}g;lid^b&48_W~~J0gs*>Q4STyy%Tmw?%SA4VllzB8<%+S;){6x#pm$OZ+-`p zf@_yIaem_#jxh9xkMMVY`=9XZcYlMIUj6{B{|TOd;Xa=G+ShRH+BNPnDQ%WAfu%B1 z3T{}yh7=7%Zn>yHhkLcUSlB-4jR4U+{(ibFzh_92lZ_ikJeGlD%IeHW&NG@42K0Y9J;Ib^=QUJk~hP&kLbjAaMLu6qQK1z5~2$LHxTCm459h+mG{# z<1c#qym+G-R)Qpr8KdnOrih=Q+OfN;%qK3L$&1`K_kp2<&sgF4U6W?L1%aAsLR+$o zy6qHJxsSw+<^CvaTAV%B%~8X>?{3Ue%UPSyi8>6qzZhk}x*txir+3Mv`vhFC9ZT3@ zEz{(DpFkCmdAsUZcgGd@vrtCkAdd^i-L0DKZgee)N@2yqQ=f@sfpj+_ zBzCfcG_~EC_GUhAQu7DiA#a)~XX!c)Qw|Dxvl&Y{CW}ghj$Lt%Pe%s|=(cv%&44em zx*bu<1$Ns}Hbt}P%Q!z=cxq~K?e}13po$YW{X$EpjP?xMb_Y9K-qDVz+lK9M0TFeO znhauI)EQG_K&7F#Cwzb-PF110;q2@>zWL49@y*v?!`0J|aCJQ5+QoG|_q7L*L&fp) zFY(jA`!PQK^a=j>PYNEt`U=i3FY$vP{t#b$@*zI{_%mPTmx9Cj4gAq}zlT5i!5`z| zyprPK)v0(pgp0!g9ftks720t}VdW+wI6J?<{TH6c?dR^{{kJ~E`UKzj&V6($xjL)4y>&c%@(5SQBR>4#_jvsDDm^7q)W7zF=WzGl9encPdr&R7 zynYk6?%qZR@cR!wz%PFO3w->=mw0%*!o{td_}=$l!*+gw%WKy$y5Z^Z37$TEieLZg zy_Al79)#lh}(#yDY>n48l_FK5>N7Nno^3#v-_8Y&%-YYI|-^L3sKI@@P(Y1-v z)8g*_HU`elujAUy>kut?|GoDxhT-<@+j#cbdoVNn=C{Aa$De$H);hj;_yrz4dE#6o z7`C$ul(ONtKh1paVaJ^9s{*|Zmz{H|JGvJxS|nwuMK_3aH^YuO4hl+e=r=mg5iv=h zOL(&+(-%EjA;vw8O0Ec(^QuxajyZOdYRrcZ>M`RbI?o?j-tl0fMQYy~)Fd_&JpJnI zT#}DDiVuLN`h^&|beiWCF)K2c@0Ph9l1Z=OTpXEYKRMswvr5G(qYs+(EdFf>q{JES zJI_N=LOB6db1_W>UZJ~0S%u;lj^22&lu)S!pHMyQt9L^k)_@ThY0kb{%f+N9sHg!d z7eFJ6VsB^HJnNzby&Zid)0kV#|GY&`Lh9aq&S<>RL8XF?@RZO|Tz#Ur_kD4LPZW8C zw1^_VPOzmyK_Qg622!PD_|ISh+msV#do<71){`zn6_|Ze$zIhYhdi{0W zfA$%?|K2-z^NpY3<4@iLh?a?2_0WpLwF?|=$6LRA1IObL*DniDCEeL$H0-^h9gpZ^ zpbf$8ySI7CE3|B|QgLx{9e1Do8m@-m~lnwj7BNn9Co;gZ~f^HaMho|S#@|Eiel^@ zCS+)M{P+vJ``&M|Xc;gRD9+C>ar5R)Jbv^A{{AOF#o75ay!wsTaOc^3*lpm0k3Ph{ z@7@)d;jnG)=H513zkLTEfASF?K75Eb-}nXo>7Rb?867cR*$(bif#Ad6eSp9F@lWvN z(L(@&zyAOJUr4>cz2{!SpZxP5=J@rh9npsQ!N&y0)*EWQz~Sr+wN`xo+2{D=!}lO! zxOe})fB#P((-BXhivd?xPqFU}TiI~?)-Ak$evYfFheTc&`c5f(kFJCM4dzOC*E$m5 zs1(5X489VkI8#7}4bSCffn$R)vb{s#BeT6lYMw8(6SL0|Ql^pe&}`-%KxN2Ob{rio+lObew^s|p#-J(|(Z@khOotk|}VXLD$YDV3%fx}(Ed^YAH}hJ`kSIRAQg zS+bA#c4ByXPFHHq)?p4yBdo0!b^ALH&_gLap#A00Qc$eWkVSTV8o&JhtVBs0%{?l> z7*izn`wXt# zyu`!Dk8pAK67Rh88@&GYZ{hxf=Wu>@j+b722`|3-0Dtu#|4;n>z4tixE*LoA`i;A| zb@vXA?F#R`{Wj1V_NyyA`r=D$1>U7>*Kzau0XJ^m#Elzgc-pVc>MV24{kZ2 z9uzxxRL-Z(<^ zh}-MeFLCegO+0!05WoA?I~dKSx*vb|JAAfX;QZ#mlP6D5DqzKM{p=hSz%IbUFF(V7 z{^{S~AAkN6eD?Vl82gTW3>>y|)a?v6Zr#C++qd!f$rF71(MKrTC9oA7E(!z<4vkScZ$}4#Og$MZTli%a%<445B zkl!@?P(CZgMG)JDYiHL`OJ(-Sz}2zgaDI-vckbbpufK{{UU?bVukiTEQEgYc=GfSTK7wHD;3u-uS2Wi{NjM~^K&-pvCws2O=E`m^jZnV-qB=VK%bo#Aq%7M z@K7NY9uSFOy56!o53K#LmWD_qk+uLja*Ao*JJ{qG{15O(dn~AD5 z&B|MtK8*MKR`F0TVHeDJ#u@Yb7e z;o;+_j-W=tg9k6+8-Mg|?5*MFKm9Q-uiwD4uRZGz@!{ZIXglJaU%!hTE#aM`3+h&J zc7B1EUwIj?z4{WqeDaCwQFY9Z@z)Yk00Uk#vSZqcTZ38qEW>={#q6LzGq0AZkiZ=_cr(;x9Ns9_6&dm zu|Yx_cPYLrg$9Y2k;&qnOGCrqaPTY;bYks!xc3e#;XTqEFSXVQ@#|U0xd@ELS&oc) zz!f8GZkXq_WCG^MghnyLRtlO4&3xg`9wcqpGL4ujFNp(u7zH=+clA_T2}+LElAY?? zXY%@FF42=rfZT%^!{v1Gs^v&8jIcE8rb|C|NFfMfs%FIFddKEnStFu<2w|?n5@x!$ zBdrXQPIlFxN2pWS%_m12vG_Hx>LQB0bua=tBWeA^QQnSoF|Bj3&ckpz#|v}0-5@hC zx<7QCw|lKMFNO$xQsw~1jBREH&x2TPCfKb=3}3=C1Du@v$V3i!V7fqw)E<}tMUmWR zOA@-OGD%J{O)ez9vIfFbpj*RM&X^-1IVX+4tYJ=KjdVX(HR+6!y%4ceL-{aw9;Pvl)AJ?C~gJ+((j(hiS;{W=0{|DZF_d~q$ z+IMhq;|wpncpsPdZsYp(3p{*$1z55K_g-~T;cd;Jb-0lI+m zZ^>9q3@>n8w|?e%2HQEV-?)LkKfxP+|1+1kinr@B%oTh#@Z|Uuy>}dsSGa0N?EOeZ zUT1zx%#Jy}RYR!<)J^f}C!gWRfAd#({NxF)U9+sR9S#ZXZFgST7&z`%Xk(ykisRV1 zTkMX`?%v_nTHNl%W!2+{kMP!;Z{X_5Qx+YZ_p4WpwQjh)IN+&y$GV+uxH>*gA%N`<}W6^?Dk9*KCh z0)6+W2Ni7Mex5)YnROJ``iKgj+|_%GOxig1J<Vl(dcgUP~A0l z4Ovy0`Q3ZY_QPI#pPLk3NP^u}ncsKsIeS>^wWv0Y|h+i>sGyEr_2grj4_{o9}7;iva;@!Srq&vEImQP+dJ7K^k8-5+{MBdi!pkq<_-Mf=AAErK-}?a5 z?mk|4;RU?<`pdYuyNloZ&ELe)5pdz@XYtZ2FJXJM#dp5_Z9KUD07plMROL8&BN$3B zklQ8LiVFyK>kiKDpTo}HF1E*y@b-^>;0IYG#{n4yJGa>nCiU3QDie9cs^$%}aY`73yM_y7kF4$vEO8CFrMRei~~%)UFB_zX*Al~wvz0-HlS!0?w@ zl5_*|iu^f4EJLrCb;cyD;HKzU`iuz$B9mXlQP@&(7Nhp?_ATrnH^S7)mE&GXelYV$ zfSpo(-`X9i!ivi=;${ISb9k8=Zz2+JL#Q0mHnWzMj0tI$oj-6O7Fr~SRmQk>1h^%} zQ|rxmYO$Gi76DFwzQU0H!o;1slWpgWEU@8bKN z%7t%mn+%Gboeg&P_OM&_aD3wtZohRC+rzC_fe{=8#imrI$~l47e?P-i0hJ?YIY!~{ zT}vTMaO;b^WyW^v%p!f@OO|r1p}t;LaqrZXJ6& zyFgJlNVU#|a?nZ}w)4V8sL!o0oxg;u&p(G}p1F+ee2jkN37bd9hgg=Ghz)mE3r%1R z%`bt7{Ov~LCpg~aD77fgp1Xi&u0D&?`#U&#e1M~)Ll5f$Ne)*|&z{<3orouqN2s+u+G3E@89TWIXXy{USWx9^v7G z2WT8=5%~wzif*3HD5y-Oqv_5(3KwE1Q{}s2*!GsbC~4$8@h~q$%5=u2E<6)FUV*ZI zRi`0D!KLs;xrPdKc9DV>B5X8Lyt3N&Ny6fMJ@{zHddIv9w(4oW^R6olhGG5-zYBP< zhb_Q4VSRKII^vN-l;fi?$I67f-f7t3&EoUPAh$6J57EKG-Knjjhc#ick+l-0-?5+J z)TnvKtCaLFMC6VpG)jAkQ;rupsE7LI9FZc!DlrhDXL6bG8AODHpfQC+QMYovDngjriAK^?goh`$$*x?DC<~mI~Pn6uiQCnTw~&W7A&d5SB)?rCbRKp z4T`OIZaz)s>o$52M71?{!ib=(XvHvGkTlx@ZLSlhYL=h-QdE7^EkbC8r$syX0{Y;w+psMhj?)B zQ||&Pw1|VZ=!8?Jb|JOm#`PQc(O-TK4<8?3US<@^+pE@`<5w*#3ZtR>km?rN^xAPWL5iDw0jvnLq z=y7Iv_r9PR_naX)?K6=VmCteYj1z`UMPjJSWlLtxAP%SWni)-boM`w9f@#`dX+3jM zlo={019GZ$N)BB~9+Y5-Y!H!DLzd!)-_qxNe|rz*=D@Z`E|BWvcqfVEN1h|RIMaK! z3S^4&Ct?f2gsEcbm5Ci>1NmH9v9zt96NJ8ka^-k&E2$B#I*T~ov1buND_t2jx|n8$ z$`QWNx8f`_P~}=jA}JcPV;r9kxHMR#v)gr|c8O&@gSV<^W#)u%ggeJs$Lhtw~# ze~^VrhNY3hTL!RCh!$zakds_W6B}Ti{sGO52IDW?qZ0_^HdcWyR1^V{($wx-S1y_c z*4S3qk}%Z8;@AT1Yy)GQgpQkB5*CCzNIKR=fGQ z4eB94bML5S1BqBre(seXivst)lnU(a?zo$v0^9i*t#5r)Q!DCJ(6$6MmV&z3!~Xem z*giVIJ3stB4v&u@eZgiUc=D+yvAg5A`?LFJaOTu0Jbd&RbrMWfFiFRj-*4S1I6Qs` zE&JHptEhWtp#p3h&`QU&2sWDuo4uWM6APuR0>!3ou(R32-s#i0|KJ{e@Pi*>JG(l^ zV?T`}9`!?fa&;hBk*WBcKEde{rYf&4PoLVu<)z8qtBui~jE&*QaMU&dytc=^>=@cL_4v9l4}eRP0_j~*lrBgUpKYV_TQGpP){ z`STO;x*!Ex6emW|g=XE@Nh&1ALDv23nI?8Mtlq=3VBrW`iEK<8dUqZcOwdd)H}mnE z8mtwFQ9QxrG>AWHu{`B}hdyZ2P=R);JBzvqNP5yVQtEGmt2f5qD`uXYELev)sEy2Y z@hO4F(dufl!U`8MnXaSkM&ddmKW*V8qL9guA z{g#dDhB?9!XE{_~p6yYJgKo`2Vi-Y?&u`GuG40C&o58nSN zp8D}Kc)f09A~Ia?}jtGr|{|-fy)uHfyT{1m-!A-(6^{qe(3 z{eaen{R?OC@|WMh6PGXI+2^m~h1Z^eG{XmX?_im?q#!Km2T(0Id-g0|e&uDnaOE<7 z^!*=TyFJcPOP7TQhvtZ-w~lta_}Gk81iu6eHoLpnoZ3ODiW}Fj;ogJ$Fq)ie7-kY@3n|Tk(P)kS3=*jQ{^>&<34@vt=n5?JmM}wpHdIwL^epZ? zMt8&xOOkL~rVD~OM5?k{vd83Qq!0E8W?C8k))=O$9w)ra<9+bB17x9KT-t+7D;Ehd z?mL7(RDBibY8*DWUkMkADNn@X#<(gxK*o@amGLp$@K`zRJBj8IoHfrg3eC$)b4j;W z#oU|&SPPAl)-v(YyG~;gC?%+^wajOZ+pR5~S=e(rQcDRtJ5?I7L=%M-JAqnmEtW|@ zt<@!ffpkwqz{>YQQ%(do4LO;l^L%Z1j2U?1j0LMC0%RO+R1m8E@6E(JBbQ3!&#dD|e)vtX8Z+`Ao?Czhz+5LUAw#Coh{RwX0{sa#mKgP{ZuLCHU z>g0d^>9hD-fA}(2d+`NZ7AAf>x|NB40 zJ;vK_{SZeXRkM!5+y^k5L*Z!jQWSd{_;=o54?@nHC+(bhnXt^sA`2ZX*4|l8f*)$|UxWvWkVa6H^T2I5Cu1CqRw-`C=&^ zk<+P2Bb9(FQY^s`WVf^AsKqks7pwgF!gNG1ZLlSBPn`8ilW}Kqq$ZegB!Eg`^5N3* z67Di^Qni$UloI#i)sWLhm0i@MEsOF%4xY>kF>K7p?iqko$Vpm!v85x)uUX@=$#)|P z&9*UUy{!j*w@@|YZI;MDANYmFkq<(JA(=xCTT?VkUOJFr2nqsda)3|B;*hu#kIAew z9`%Gb#)34yKrLPY)%>%R0W<#i%&ikGqS(%}%Ll3U>gu~D6@ua#5(l#!ZXq8OZ2$N% z4yP@q&+I`@3(yO6RviA|0p{;5DAyGAxH_G_`-L<`{xkO=kx?J9nv)jH+avUOMiZdV zGbRzteFkicwmpKjhT0nrkGBx%INBa!nIEAq3yu#Tp%lSXChYC(;Lhzk_{sNwfTKg- zk3_K5qldTg-rGMw$3r}O+;uujiIn_-K0 znX=hMKRiU)&S=|XEXRjv^PISb#&}Jr;`Hg$*xfC7bbKE_{{Hu{%vVHOG zwy>i|(CrM}9;5W0jHEaSI~v?pG2#4$vp9S93?4svjE_F}2*=wQp6%!FMQMhc@4tgb z2Y2x3)+c!L^IyQ`sY|%GJ-{FQ?jPafpS_2J2ah1lwfMdD^fPRx4K7`J0_QHA!_nhI zT>J1t93LM!zJsZVAdPitGalW)gWET*VMk{iK7NFcK6n?)(H3YO=g#fp?71_z`t*}{ zeE%*E?%qM0XBS+U@$#DS>CF%E>7DEN=$&`*_LCQI>C!nI930@o58lU}+c!~qOVd_D zOHsU9wwBY0qLcflZ1}7G6PYRZj{*Bzn&(RN?wqB#fSg`IVuNyGurziBTMvyY>Sv1t z0C7$zVz=%IlAJ2I(|h8(yrd8-{amsCVA0lXoPVJx^-=GVmx8cRU8q7!-&%05q~#eD zokb?3+5jU4NVxN6+6ey|S)e!96klHWhNfT6&$?jS+M$mGaMLuA#_tfjxJ zl3^{u!y47@t+r8$Ys9R?=i~141}T%rMMjo}R?jV7jpSTFP*V@iLJoDB(Aw-+g22Id z0j!`uD!@CZ@j#Dp3I$KSc?rjl4)N&veH?!KJ~r=9(Bq9`2h3;vT@1_OOs}1?!M6DV zvk}X!A~-(2kNbDu!+U?dk7b$h$Bz3hv&$h2x_~d?_d%z=B&Jzl+;9KEj7jKY{x< zKZCPpPvidm2l(NSe}bDgZukf-#s!o_Pb_bEPk`z`mYY-b!i ze(X~4ZhR3(Y-^n=HoJ<;PhCRM1=l`&7t4H%sW^^(SsI@g!}0bpN~ySa=LY`h_kI^! z8o4S*&l_~?DsbuaE*{>xhFhO}f`|9-!TRD0=1}%*}8}t*ZLY6hwN-=rVUNi6om&48YPnDYA$Q zZ^OZk!uWumbn2p$cbfED6>wBWclK0k0M6ZDS2Pb;Fs}pK=+=kdB2x@jXGWYs#*Qj? zzw90$2JK8`)RGJS+Hh)NW^YN-GdbC#S1QXSm0 zP*tW(PVBv7L8EcpMlz>*fS7S~A5)^KvQX#_bDbAYirmgySBQS;%bz!6lAc!12Vt2e z)>`Km;&8O!yhQGH?;Wkrt0j}?=Z4yKG8Ts+QcN^hTjvk0H*GMo7 zvh1KE8|U(MRRu8{nTz{~B*}2F>b95To zT%(NsP3Um8-IOU!-RrPnX++gW72@7rGl)1n0qm7>JDCe z^-a9|+Do`|_XfWA-9N{6OUY;%A;a!u*uk+O!2k_`%QTfl8JR7<2Me~6wH)Dy zn42*fv+5(&IX_p4Y;%f;w(}9yPgCaBfD~^P_q6yszwjdQce$Xr3Y3}YQPE@Vo#;!_ zE(9f)&tYJ+&JkcK{CC{N?-0f25>|#z&GRF)c8KH053sby09kcmRRBpE5yaCwT z*}>l49wW5_KjNMpwNNnnnLPGN#`M`(At-uVd>p8q7whno$d|wPbJoqE1z1?I1ZQgG zQ5W<+=dBv~*pdC+nY0ygwR78Mw=mBOj*pM==;33`%aWs_Xf;DS9g2+%X%glz6RtQ~ z&{(!{aix+{WB-)Lr9;6iXOLqlAK@t(0>!UvFl~?@l&$_=YM>Z%TL2Vj*~Mma8l`Of z@bBh_UyD3;Wh`*b7O^u=dudz3B1ybuPBBE#us|nY)ah<@uqC>iwOWBKI+EX7^D53F zYE=kXlN2-{&=e4KOfF4jCsw6qU>F#ZQ>RMhjDJSqP#wsb>NHbVw!4>7eF%=Y-B%oW*n##O0A6} z7)N1u9tO4e0@1ln3uy$A zC8E6KIJb8<_4Yo79Lj*VQhg^G&g4=H`Z8y?ABMQ#y@ffLQDUx)l%bv!`YmE?SJZ>! zIsu>}iV|{Cm1H?}j=SO@g!RmVhZ)4%Vwf!`#bxtxSye9C3*!a+GxNRWLTR?aQf;JM z;8$hX9o#n)3eWISloE^T*gpW)w>UbwhwagQa(vZq001BWNkl z^zI%uyBi;0t8f|aKgUKl!y+%>_*T$mp|XxBD(i!qOka;mag`bbRTQw7qG&(R47|$?y^cvnkMq- z=oz{r;_$mZ&%{9pT41Y#T514n@!gOA!WlmtvA~dyNgcxhE`t{)BN}?mvR z_4@}FOufzNsW3(kbjoQ5k)n|_P%@b#sI;mDvNUITHRD+@CLK$3Zj=hgMITnR*ikw+ zs*n5}DzrMs=%TV3uOs8O<7Y{G0ue#$_}(BT2OKi?E|vjxmQ#$PLxCm{YMNd)TjjV$ z(BI$#dg0QEZXfS5qGJ<7i|397@n)*nHjZAI_2K98W}u%))PM=JTV67agpo#eVEBEs z*#eX1RLYE+<%h>lUQ{v`61*(7%j_|fi_5~?#d?g^npZ*+qZP>yB<=VZU_|#$NP-6a z!SgJUP(3MIvrv>Ex)td;#u#6$4R?8dfm|Y!id%|~FE~bWMOGKG62OO)3l@wVXy>dA z?v?Qn^y;Pzz80|!z57G8#!rWD#=0}NV$R#5y6Z7&Qxp?O_m{NNNLWAzu9UpU#JnP@ zT2p%-fmZ==Q`Y~|jlI;$J#U?JWs(I#0Yh|%8X(W#z`lFE}w4}+igH6|b5#UIy~Buj(0ESbA~9Q-BM~G|b!8z!`t3j2 zJa0YV$a?xh5_Bp4%vGuWcbg*}mSuL?D3{{BN3v%3F;JDf$dTBe?p3JtRR`2kC0J|8 z5w0OdE&lr!5rRy6vbMBH{u!F&>?rCeL+x_HjMh2kanz5Ga(I6wbE)-FTBw0xLXytz zj7DW}Yls4(@-2MMgfK?KAbtZK%Q8EFGRBDT`MkpSs;xPHEZ~lDl(xP~Z;Vlro6)jJ zRCQRS>ffN-FR2BvGLeg*>Nq)JAUtK84#NxJyH^}lz4~2<87Zp% zBB;lviWV&M7V~yad9HC59GG(p1CkUj*CbuUQT3)V5rcyhCOwH!7H5Z%UmRapwATT9 zOM?^*h>l6-Ax3&=AXcJN(!yn4_ZLIS0TWar2^6JFG!PQM5D!-=sHNt3OeHGLj%k{5 zsUI`jP^bk0}Tov&_SVpWkWJTf)y9^uV;_+=uz$L!Oy>bU|Yl1$nIeyh?r+ZsK!{3%DFL1gx&3nbWFcM(yd1 zuCCL--^AXlF(Q;;(I|M53K5HECeIniVYL)4Mmh4ZIS(Rb=M`fgW9K9?D9}dv=?FPO zc2iQ25eOQ1j)*ykg)m3Kk(JS!ql}VPi;;!lqj4RaIlkvEZFjV|@qCFcOU&}w5Bv;l zjS)inuL>*fxTtF({d)xq?3BIwIIE2jd{DZVEtbXicbOr+!jVL$CM+5|b3yW5$vxLN z*w~-pj^5F&kV-K&oa)B$Izg6z!i1h3 zAPEOq*mL}~v;|s9o`cq1@FODFlquPXP(k&X8*=uTrwf;AL9MK%0!y%_Se=l+!xEa< zDif#aaJs{J>8E7Vpc#axFIw||0A%T)lfj(jt|eyujce9*$d?}uK`V%7M(7by z83rg_SEw=eVy<+I`W8OJv1m8W>@7Q6?>zc45dT?ovii>1clT6JfuOG4SI2#L`KF-k0xq(V#Wn7nbkgaBZ!HBtU54K1hnu1wn8u>S!=~< z$3jyhl9t@K_i_x7U*erYB=2tORN`iA^d(3tVs`3;)(yw)*ynw<0F884`22Hj4Fv@> ziIU+zC$%;Qrn^b@JUn_?%WRU`8XC4PU0lmnf>gsI-}z zQ42>}{#{m`R{HH#u67?wR$T?nxQK1Syq%%7qBBatJ6Au%=QcAQ)yFVPYkYR-rQ#0G zx}#5`vXnC3J0x<$yYjxo4saD*6p;>Q&@y=lQ=ie%(YpWs9u+XB2g{*+J$(_8o$kUD zpjdGlgeX;m=4`D{i!Cja?-V0ix)al`D}Pu3QM(L>b}V_F$E8r|iI3@>2PqUH4a>|$ zHOJj~{zbt@rJMUJkl}V&2!+VP1_#giS(e%D5~e-VJj()K()$v5U8H6d7CL;Nx$y5R z?iDZ+Pva7>)neU?MfG!;`yk;iIu`zE%@7wpn0DrpG&eE!K_D=7=w~m5sfQe-1Gi%?QfG%KNA-Mk%G#;f8H}psTaL8DU#~HYvIO zNOEqTFqklEL*6&EING6oOa)HSY~!dtaR@Bxs(pRXmny_ zDk&1_A=iq(vxP69@v#f#PT>wnaD1%h%qg_iHEi9%N9A8_Q?N*fSK(q&eqWeAo(LgT0NvvU+ zk374G=bNrPWr04)>>(SrY`wzIMLD+Pu+$PPoRfse zZ3@apj2TA&#`w=vm?}F>{ySGM$l_cqC8EQ*Hj)bTRL_$8!lO16x#dJLsj+`#MD zOEEu;8uOeZg)9!Ln18P&$>ARNq(RFOSxcQevU>9No$Ypx9@adX7+s7Ps!$kEh-!{d z9E+-|Bi*G2U|zP^Y&K*9!7(BTL~~(Ck+%duaX+X6KI z($mlgUxX=Vn3WHeFB2;ddqJt2G3v<(USvkN5JlbT6VjrN$LFGVo)K}n61liRKo3TVumS(P@DZ=J!^QbL^S8I3#^b{pDV z21(^f$H4M0^FrG5Fw|;X+#IvTi`^-ajb$WOdoW-)pe1C$sSZ;LON^CvMO$11uw+-I z^zMYxv1Mc`B9N{7{zi>zxrh%S$yLN$WIoN0>9iWK+PP!sO zgaXv}NgyP)=B3j@m%*uIU;+aj`syPne{6X__#g%YZfy#-WM}B@BH<1MzoeQ!ALH$6&s`D%xo1 zWYN)Js7Ah0r2b{aWRCM*touozmNM8vDWTSaM)+LoLuYTq7x%Ux2YXRfcK@)aTC>ew zD?o-Ewv0$f9ixko$GTh?!y$@MHASk|x}#X{RQ!TiF>hpLY-E%pIJXS%gfDv2tU5di zVE770Y5`V^N*2)MYk9;hyDz?Az5vFFI>I>qt3^|{pH&yL4vJPd%BGOq$>1f`47AE! zL9}c(E)=2hZ0)RdC-Ima$D-RmznamT*%8{1`GqVxs!Z1b`9oY;dBo43RWotqG3r{n zJCIF=BswFE!*s~1%fqn^)Dm+sXv))?HR1zE;Eu|s7 zGA1Ln(Uo=KX=8~m>ysn)!7`Fsb4026d-bJ3>%=q8)sk^mRbH@O9C*QAl_{WQ;Gg1I z6ez0mmylJFg1NyeXp8I8OP!dH!Bj$0DLY-AbMuF`V|B<;qrP;fXBHGJ((`7W+cJzm z>cl&1;gl>HjR^x9<{?Fm)>?qbpN~28;K?9e@-nuoCo;juC(jvdb970h-Gw&Z$9TS4 zg$&h%JFIzl$HFcTGQVud6@W;=V%}W|@h)Z^3zTzF9w=>8fw9z^>f~B}HCQiWT&K!C zMuTXR37nC=)j3(b%<3WZGrXXA86{7Bk1xNCT#dB?!D?PLz|_ z9Gbt}T4CnYUoDy!b`P96;7eWZe7aM+pv9jBkAq$zXN+`a1*CTX&4HpRwGH{Yt-{~b zA)ImMpd}?BjHqD_6{;4)i(-^6yNi8s)gVV$@ytgwMbEu&&+xn~&UEYzMXM7AY)-;} zMY5xXgDe!L&DmnLFrdsA<|d7-z8NBpCu#JL)G~3=?jx|?X6C57qSgfwWPs^}TD9^r z@TFLzWH9+ry(iNK(uUJ?X?cTQfh5h>6cgO}5oB?uy-th!MqJ z&{cd1+dB#hRdN314^#se%Q{jXje%N)2zn_)A}uOV8+#gXDPBW<9d2?(iW!89aEq#u zKfmyzwAG@T3Fb7{1>*-f)-htMV=-=4Se=<)P_26|MC4jW1y(jGGdHkkKubWzc}XJ9C;KnEOyck zA0(5Cs1spfCJA%m!~9_cV^)eB|3xjw`C}xQBP` zTGT}p<{lzfe2%2S!tdV;<9;F|$ibu0nGa@_!ScNiFGO|?qi0OWMZ=2BK!s~5Axs&& zu~E>5e7Iy6oj((oXaTe|LTDv&LWFYfm5|FKV+1Y`3dUflwqmy{>idq~JF}N8cRB)w z{uNEnoG5Yj=qQ(;zWnQ+$Q5Yj#sfA&n|u+jMKCYR$Ob`-SFmzfYND8hizGnM=7#xr z;7!t0n7P5B9tj+?tv*aHnG;>0|LF&=Nal?zEQZk!JdC;xMt|~;IOpy3J&4k{$mMUm z*uD=cl(zCJQDsL_QOiztB|S!$MOlT{%<-;8hj1X0z)Hz4z3||}c&)~GpmBN2&!RC` zq34`i1_3}hpH2g(zv$_FrGg@s_Ct(-i0?_Efy4`$Kt4<5T+BvQ3c-5*JM?t(`qNw3 zQF?;el)s~2W~&!bLhfBt0&3$U$XEcefzc1&g)uNE4{S2PWc5t;XbN@pnX^kM(R~(2 zW7IOG5jGaJMmmD=&(5VPLW$IenQ|o~?O+au22y6|^+PBzlu?!=xp*%!AHpJ6V;Co6 z3ED$+!UCsA_4oy4PLBEXAb!7iZrZ|qOJ5m#V{gS`NBHanzpd!R&rHYjrEYRH8fJZe z^^#u3KpVF!ebG87$H;3h%zs+ z%ui`^L+>-%e3WG2_Ooemr-~y#crk#yC8B_%l`n%#%tI^=5r3!4YC6gT zFRJ*Q`4c~H@$kiqQ0OrCx$qK(LtZYOE3pVh5@K+cl;;kGiE`N)-CXaHmRK@x+jCPK zl@JL3#7iS~P&k>%!ZOby&xP2MPMy839O{kQc3bF0kWpl`MlD=3zce^%>=~fs-yM2< zk3l;1-#I-%=Al5GGXSwnk=Hm6FBXylhvhJ@p-*HO2r}?ju8ee2%s< z`GS$PW+CNgm&!>`5;S9gkqzJtR?C%6jkBwnTu;0y6OM4iB|KI^DT7zD zFs^d|%PMo>Lg z<|?F5Bx_9xp3O)B3!-W3?otnu@rf0TjS94PU-G7k%_7loL}L~$km_X308rLxTFsj~ zzi1?gZQP0qGfi}f1!jexo7s|8c_>m<%eYuMSB`(zrG3?NT9`KyevPOw7wX2n(9o># zl35fZ%B=E9K3zq2B0u-g)w@VxF`v%{#b6zfBN5%pdAv7$Bm~$>O(%sr5FL~P0iE^S znF!5Q7K>>g!Bt*}2tqD^nc+TKHu>|nWf|Gis=f^EouOkwS}>JQi;oTbcVS0H)zjRX zW6N0y29db81yh}}TM!j2b4zi8i#8@7`G`w6XA*D0-^V)t-QqhSHb!YCrHtRBgl`5NS^(R&RY6-(xZlM(T`=;`BZDKUdsR zV75#|MIwq+3RP=uWO0b)?!>b57!veQSf#TZJtqbu6>`<_)snj}>x++?%?*^9+fMaj zjJ`_-Vh#0dk(v^7AA5V3RGnFbRV!w6coy`TH*j<1Ku5=>R96%67G9_UUm7W%-~qxf zyVeQY?cz5jY?$QOt{TEn7SdN6pqO;n_5mi8p`(qL5-0E!l4?)Oaha_$$vI!QMjwK% zT4GzND|GAxXg~tW3B?f*+1$-5tYBxA>U$Isl88JNUdamdKqWx-74>q|lB}3`-Y2@3iMkgbgLSl( z&&C(C7SjnL-dlEsLhpu4&Wlt^Z37@0u*$w#iqrGJUnYnLU=EM0445HT_5de!^K$GQ z;vS6wtS+Ww2@n2w2DnHy{=65p`zQFE8_ro;1kv_ug1J;NYnKvZ6t^|@* zr!bgNfrO4rBR@>hbih*|m;5c^QanZnEety{InJDmX3qpcA{(?zjywXW1`&>QT2ehV zkG-KwyaDJ)v$;8-#0)93wE zpa@(%z`|i2#1&nXP>|qzwAF>m&&1`H#CK3dR~Uo^y0hc(?ZG$7?@=j+D9PfIaQ$W# z9!YcFnRaGVgeamAbrGF#%-@AS^E~^4u}(FHZ?FMUL9ISBhK*0(t!=`@K>q*`}0pmi>Q`w4M8G7ntXUE7$Nm?W@q;B~^3+eeJ73|V=|g+iiJDKQM0-JZ0~>nV~(tjCllM&SrY zBGtkIlDJ~+gc%fmh)t`*91!2<(1j~zXaFpzse*hd5GozzrAY|iGS5DLRi;cb-BIb; z80J+=LmvSH5oBfq1@OGZvpCyOdFs7o29IwOf#bog$JsP-6rgn2EnJMoqZ z_3|YO9fJL$2fd-cpt``=171la-H@lS5 zTB81qxg=IvPJj)C z8vkeo0|V^}5u=k^(8#+|Vy|5vXqhu&0P3bFB4fakZE)D^RBJB8=}*bqZ-6J4F5+wdD8M z`vMiE&tp{x(4ydmw!lqHqr2l}YAQ(vj*zgzd9iSgzAPU1sf9l~sR@yZjml`_@Z3nr zjF*sKYRDnku`88JN)kXI!L|6NNsBTcoLzx3b<#TA^Qf50z=tTw2hGOFMgoMYh3Rs{ zjI?EdMsp8UM|hHvyjmw77~({20v@LbY>MxRMbBaj_f`XQZW+Wxlz$|KvVbUM7qx22 zMxg;Y_3y2i0maZ8e|GV-yiUkvsTFPBQXR=l(1sc_#@DgOH12LZ!`zr^*wf&ec@jng zCC9`l1=5VsyDS*>&qDAroKGx%g)>Ou2YO z8E^&&i`7F$c(WUUr6>lJop*jlijdNCYSahDY5dQiSlcjB!<&YByxsl{_oKtqT%FU< zsQ`!rM~Zl2T#OY|`C&LsvT$i+bgo--m#eB9(qbGE2u$6LsulFr3QE&ME7=G zO2N{H%T%Ru2^kqBWyPlQB*3K;10Ev|6rgZ^ZygN_46e`j2ZWcA+KEdEs|8yI{iu%y zQuV>O+fc;BC@vXL%%BFB%AM@3!Y<5l6%MV7xolx>go7)GZ~+>*7GuPgi0+6cj#+ig zaWP_9A%QFRNM2tOjsT$8o5b(hr7?ckaVw%VG3JdhLLqc0x}NxRDq=K3OoyqDOR})> z7k0K8n!7T|fTS3ol%UJ4Tyxf_`LT5hI zN6Wq_R`e!W;2~Vg(GAZP~iswpjI)*s4(W^?4{lN zvgJ;P9YO13=R=Q4z&m{&Uz5-bo%vjK-^DHqm4-`7&x^Q<$+*vq)-(>o2(}uUjA1)y z3#S$-&5hV?J84m%`#L4g9NPtqlsYvu8et9od5l~n%Vqo^NHf;D7n+s5>nVjU`H<-b z87_9k(P?sJ(9;LxVu-@fH(Sxrl?L!4ig{UlG#COAu^jz&?(E#g;MqfS$g+)w;8IdC zTkBx|!bDxJN`=7E4a>5nc3d-C7}Rc8)D_O!fBAG@>X<8b<}~=15Fti(0#h=8h)5n1 z1gI-j(_rHCB;t;ZxusLRx0dk3G`2-f#Tbfmq}3@mUF)&2`gs}>;63`$!P4Qy(&=n< zXL$|MV@hTb#-5Cj9A-K#6?1FM451-0W|njW3l9|keL8v6@O?ro+MTFHXz*0GtU)uCt^hz4=`gmqt_PMaNU>JFFhl=vPm zM4fxJT(qOE=-Pmz;&)5M+-4lj$7sve$uipb0(2Gxd|gmc%Y>=yVsEnt6~W=*1GIJs z9Ch@zrO&95cs;NnZ596;`7T_C6F|BO9iz%!dPe6K7vah1iLhVq^>XmS15k66AgL7SKIzVRI?yD<9!3evvpRuVqeo)GPgj!O$;)ML_?_l1@v_oy3rLN-P0a4^YC-Qq!=CffbzG zpQxV*RQ+zJAaC{UUqfpykd|}%8_XeJUFG-}? z#i8l=ik3<;ACZM~1FL>Jl*}}uDWFB)V$||0hi&lsVc684#G+stit(eM!-nVF2 zfO$Kz)H#OL>N(v8b@GAuLE_|GCK*=8hfGBwR#EFF4RWmWu`FAtc6N{*&<%=ocYM>n zKW}F@h*o8uxI;lp#yZEoEk>uEDVu_BB>&dRXTrxiwd|nc4Dl^qBy8t8!MXDnaPiVv&z6~I z-1y`!ZruDB$MYl1ZD!7TC=I!b~i0b~*9I5gV%U#_&+LS=)lJ}k-j*jSRc)!b8LbQaNx zI23U#Hk!{IDtYYhy1Uy>pBrnLX0Gp_b9=1R{-%dzrA5##}JLa7rT9?rR-ZEfopX^EG$ z<|RMXje``)5-VDOf=LAXXU^cI z7tY~{OK0)^hd1%|+aI~Op&7RG76!vKZP5CPZq0Y9Rwpk*eQ{nD*_d*NBU@WM0LKT~n%)?72O>^2$|w{`0Tk`R7mL z)UM*wJ4g7_Z~quS_~ARaar1$zh_**5@{Ez5^OgJ^m_iw+KA$gjLYWk2PwnCJUwRe4 z`m0~Y)vNp1+Y?v=e)jHt{Pu7D2tWSON4RzO02={zHip+;e+J+9r7z*jUw#_o~LL7uubhGk*Bj*Kz05 zo6xzUl!op0m`Ih)W5A3(H&JryfdX9U`Ui1s*uxARPm{aD=P&V?fb*O@ytwWTY!=KO< zyJu!aEG~1Ll97>?5VP2sM4m@t2r{a(r?Z@DWe;c1Jd1CB>udOD|M*4x=&ft`H~;2$ z@cu_1L)j5V#HK5*CRndP*eVyYRK@mq!C!sv7JlP5zK5T_^AUg5x}m!Y0v?a3`Y$y(&)c zpT<|d`a1spKYSgZd*eLLp7De9{@odW@c+J#-~Qbnu(LUhZ+z=>_!s};HJm%U zgFpC#8~C68$KS{GTer})x`ZE6b-#>$Y{jYxqckbVV+06Ljf@xFm+H23@KmW&H#OX7NiGNS6ioM-k_m$KM z%duY`OY<(;4Tr`qwjeZiozDDFOU1smIQPYu=;&jwNdZl$E?sS>IjMk*k(c94^aLg- z7;-@$4@tFyDr1UcsT^$GO`(~MVAFJkvg6 zZ*x^(BLgOcMJ{Jvh3=VPnl?1NHq7(vIR3UUV`oMy#bxGTKE}K(xde3`xsO?t7WgIU zlfl{nmcDStrz3N_MVH}_y{>j5buE1y49*FK%o;2gBN^-cMC5HOzg(-1l+gK(hqP0v zX}f?oV~ZDpX59xc4bsM5f&AJJc1pF z>|isU#b)OWo_+R7oIX|Y@h9)${=LU^3sVhMcY(5p&wlYG{M?t$?0-N87^O%b3 z`lh;r8HT#Mi!=Ls_}VYNivQ+c{yZ*Uo)U{uRPfZ(6MpTVzk;%<_>JHCAs#$z*uQWZ z7cOov72v|9Q`qe6IDe`%z}&$cPVuS*tu%)N`%{Uc6E+)PIHPw|orp`TxOnj*{>gvw zWqj*f&trGbxk(2P8)_Bo?i9TA;x4W{^BQj5I>fz$x1qZmeDjyzz%PH}IZTt_aBDa? zn6cSW?4PQ5=5od5OV8r{_dbIgHxBXe;RA<0rbLx4Y!X^)@nvZ#6^kv{J9P@b@+)7( zKmE1O;_{OPtpPW0ZE@||BRu!~KAyN#@zmuF{=v6i!Uxyx;oa*G@R^@`9#^jJV^e{9 z_d34&AFt!n`v>@2zwkU>e0~r6`vsr<+*5er#Vfe~gGZzYd+~YU(i!~izxM?^cl8vi zjMpf#)}~T%WMkQ02pUcD26v*FF#^vbG8xgjkx1yBui=DpSjBg7YgHyzodj0AvcJ)B z^vGqGvEx|BLxfUmO<9W3t!VhAVjnS#k&=o&H(aje#xYjutFWuscZT}Mk1FA1r9`&- z_|A9TRA;SJtEviZJ$E#L_zzFiR2{6lbT^n*N0}or6ICWw(nCII!nvS@)XVkQfpM)m z9G<;3Xcbsfx{4S7%Xq^NnRCkwVaI9ok+Bd72F&6uRdZw#Egxu@2_;x?ZH5_Bf{2iK z4y^LkhX%gn!m|=c!;HK9owTj(5YZs92v6N6kIrW7N^df!O_t% z+EP)fUm{EQL%QlIJpIgb_~-xS7xC4vK7mV5oWc3?1sesNO1 z6!#!1>*zA!%&C2Rk4Q>Z5Ib_R|j;Oa^pjTO2uf7^M{Ri-!FN zcJc9<3smC~D2r`bj-A-e*Pc2?Y1I@$v>8MjasK4=e}WCA+jq{_I59pnt($4pj`i!! ztYVsfl}pz$&{8*bwy9?*Op`5CR$}y2m2I*RCZ66jmOWMh7%{c@Z1+z&xl~`RRmk(2 z;2SzYnA*sUR4A)kP)A8n)3>NK@lYMt%4E+@8>^RpU&)-s`#5NiXfBV(35MQhDPNfe z>S|LlNMV}~DA6io;3V#OF(@d(ErFgnA7=?p1wllZT*z*81f+Ri$7)3Rb9Hc2o!>IR z56%1~ddNdg)+SHAF(%q~adMh44Q3*1yLzgFIZ0Z6su`>y`jS)VlBtl;mCvO}W-8O5 z^XVp^Km^y~yho*fm*9v3c+6!=Kj<+W4e9r`QME%UL!oDR;^}+% z`nQj=V_s2JA<4I(0%c+1=kDW8!_^*REwS< zm`OFlnAihvih`NhS$53N&}vyWx3;*px=!sXe3+nAk*$ZVR!O(hWocmt^Yif(zp=T+ z)^?xqWJFafX6Kf;_x^npWo%D-?X_$C{1@+2JH-PJ4fyAOcqi?a=AKjg+4YMZ4Ei;t z?J_r8vM}Fbb$x^NwLU%=9F9)AWdH6Z=I1)BZqy91?Ag1*{sS{u;L57w?|%F-XV0#& zWBEEhoZ`Xz=4o{l`wlG7eSbaErxBZWSz5}nWOiYWy(>G|y=#%xo2z{E*%kV|0T@eB z8d_z^zI_KdcH{t^PDxc&Y;N4-%$d*W4Qs~Eara$&*tM$w2U{DC-@bZ|b7xjLe}0V< zCzd$4e?LLN9e1|bwKBueBP(=g6jcSiO~>1BoaM@uHT;bYE?(Zx@k3pdf_?i64(wg# z=8c=wMS(SN@ZesKpV-a)4;*A=r9%KV)<*pF?>^zpch4}IOwhKF+gtp8UAWNW^|#Kl zxjE*w*DrDR$z7a0-bHJeoh#W`9q`7>*En=wfz7QkzxmCF++5qFXc_8i+*GYvMXPJE zg~jVoJ$IC9f}KiIwBdS zdc=(;RB25LO`WaW3R01mm3d!6yEJl9$X4EFINq?_bTX0zC9RV2;N`-U8j}X{G{j|> z*1B{&7lox^4>pLNG<5gP50BPhU!hDZ;_%a~S=Q^24(>)OauZ8U3ekcnO5S^_I%Kz; z6{*jAhfRc3#C?;vEGn~+8oFr;hE=Ync9Yycu4w>$4YSGR?(X1ShK3~xDufu+N@1>` zLcV`&ifZSy9hQH3%S zmX|dfH%DCie8TB78{B>WUhX~|t$3zoWl$ZjquN@^vdiMa4xaehNgjXf2s@VAOolb5 zKf8%3Ek_S6@XCv4`HTPj9-CWWbzpgEk^3Gz%;QfUWAC0>TBT-tYsAlg`YA8G@Cny% zZirNAuw{ppU5kA4nNvLU@E&$7c5ogx*ZchJ+4H>q`gv|{t)p~cwB6_9_Z8z&%{y;i zWc}utR=dlM8{7G0xoel9+ilS*I~+ZBfPePSZfE!YSw8(_jeq@rKF7_CF-6(o;YaV} zpZxx9?AX!a*`J)}+2=oJZhkI1U{0T23 z)V%rXS`Kv|4$?91X{e#*V6T^tMKPetCntj(24<)m*#2#{d2A&e16?R%^;q6I8+C zj#>8X-_E6&trV@qlOd`M2~j~39XtaXXxo#+l8`Fnh{w8!d)kmtP7<{c>j z(~dA5rs6Ov5PVZ*;X`w%uU#~x7%bKZ110!$qwv^5O%0GG6{$2B;bLheS%;8mx(GEl zapDuo*b}IQ4HJLq*s-1*Q4v@s&0J}h)mG32Wc!%UR=b2~zg2fk45|c%OpSXgbz-O~ zKwxOPiKpvX(+84dtw3^f_-2Dh)xUV6R-tosVcI{LX#+kN?9X+j|P4%gTE{KtR)TjslM zs(K=DN<(+H#lHQE5^RDGKiFV67-EcNW_FI-@0h1MtEuZnmX_ujjQc24(3$IS+mRWJ zf#sz(t)fLywlcwCV|^Q~HQiam$@};5)h7?XQ@GSh8~@gav6*tvU#$4>0$>2K^~ zwyhbD;N=%S=gO5Elx4x8+m7-_|LqsJ@7^v&5ed1YF|=En*;&O`pWe@oQ*xSPL_YB{Oratet;)dYkJv*15jE&5wU{g+hayIGn3#&$QURJje3V zLf&vb{baz}+7`Afx%1>+p8ERT+TPbdl%aa{60r7dX}7ER4~JYjkNPLAEaixVgJaR0rFEX+q~v)2zi|NI#) zU%L@88Mfru@m+l9cTdtOh)B07*naRM3zSK#9vHc^;!l5fvq5b(;2tu;&)rA+m*%ULfDYod;5rOe6;0FabVkei2< zly9<{hxQaLBdiD9b{0={l2t+DDe+@g(o@6p!eo!;mo58<2Yry%a1hAZT}kArz) zt7FnneF%&uBUaZg@yGw>FFAMNQ675qb{_xI61IT#^$9P%e36^$LpHbioc`zngJDHk zXhuUps+;&Aef-fX)g*A_=n9MThHpG`lu13}kN@Q7Y_1O&4EmhEFl1q7fu)rWliKGd zJgwtw$XbQgC5sEo{NX=+kY~Pm8{H1{w`FOF`RXRUejo29SgkOnL0LF;{}JxIYd0tu58=$2 zD{S}sOePaXqnc8aGYHyf9{S?#+t*-tO< z!b=~qv~!8?eETH#o|#}_`vQ+VbdWdSzRVXMIm`q1?_ru&Xrxxb zE5*YP9prbubvyG5nqtaVvbH|p!iDS9bu`~}7h9aT=O8OPEJnfQ>jR#9{(T<(%AG(f zyOL~SC7?>8tqLP9DhrjDctNV1PIFd4+k{*ZOYZ+pGE|m;rrIj;NUZ|Wz$!Vow3hgk zF;f{g;%i4Rp^@D5t=Tpw^8+8m-Yw+rbQlR!gL_2AaxUcVrPO(%G*gmR6FT&Qi+9N} zmTp?X2-7RSDe?Af%jnedkv5`&#)UdkMMATI`2e*G*{*LSq+3-rWnr+fkSW?WYez1i zPptY$HHo&qknEi%-&9;(%js8~2!aF-vPr}`$r5jg!MIT6i-4X2vIWrsGnYmsxL9)h zMCqWRgbM%4*C7h4Xs=V*JP<9qP`f_c>o@q#bMG**Z63IPk(olVxz*#j=U(CL=hxZZ9^suP zgxP#M)Cy_`@4R)HAO7$qMx&CiJaHf2`p#h%W)*kdwVQ?cF6&zZCX)#!7)HY}b>*d4 zC;sZKRy+3-I0thJJ2-joK4!a6SAkbwxx`POeV?kT z&uYe#!1KSpzze@Q&7fcL@S`XA*0+x_--R7JW+=-jt&IC4&VFXOd~Jib-@eLtJYaUI zpgnVv#sd%UV`X=T`B~k_U$w*4E^^6K;PfX~Id*J?m6aLh7Az|(hQcWJ@15a0 z-~IyUE)Gzp&7)5o=6m0{gVIK`%h}IXxw^WQ(KKo1T3CxM3kq8>mY|*U>$-_VpCPRnK6|*EL5N<;ylT@nh;FJKAY5Jr;_cLScu~- zYBFMLd&qd~a8t;H@o3E2`WBN(h#Lr&(5aYI@qy_N124V$5tlBnGnvfr%U_(~;E@Hs z{KyiE3tbK!SmyGTn*@2-CRK&29OJReCbfy!UeOArEv=au%1&H#uU;MT$}6Y2d2@uX zIz0dUMee$5iJd#ds%SKw`8j6iBaH9T#Q`tA`~ep(t}~gyfBD7TsF!9z2gd~hF6f9p6qca+!yx}72~NY+@4HPN9X2b8+*bM>;KC^frxwP_V_ z^w<)2-FcL2SI;pR#hXR?jFbT{C#b?0=H_OygxM-etkIN(&7bCpJD0FFI+{*@y3K_v zn>_u*f#@nCcB`lQ0f2=WCBaF!c5T4RFMY<^%^ri%I6_vOqu-x!?%Z{rc3SNHs9P{0hr@p$6T{}yb77U9Ev$s@WgxowCskjRGcs968$M%U?RqZ{NJk7al%NSDv@Jj$It5?mEcgOe^0E%a-QhhYzv1p!vzO zuh8$O*@A8$e>7)E=Un8&IWmgH)iL`LLddzb7(MU=slRpd&nCDRvdTfuxsjEHOk=Zi1=eaT|&!HOVVIoH)rZ%|@jcjR8 zq+Vk*!O1%01)~=#)ng!{O4mbf3M!T+#o^kn$ zt#F!@){N?r@Ot8qs*(VX)e-NYU7+$!2~)^D!crAmr#@ueq(Ds*3Y`A`6Q=oDL1qa8 zS4-U$;;YVeyw2zDb(PHlA)h#ON?Kwf%U_1ydDuQRtdF{5{2+|Xd{R!y8gq-qFMG2y zos?`Njw#TH>Ik)u9StD|?{gnQaDjS~fN4(hFVPcTRi5E+h-thJd6TTF@9#x&tiqcq)_Hif+3TU&iDUB1S44+>K-nKI99534 zF22`=j;@l?c*qn|joQ}?N3|4aK~X3MgTQ<5uJOW4XINaB&5LAJ1*uhT$T(?UrGfeR zHZ$F}PzxeK+2J{Q`(FOVe|M5&M_YM#xjm{GRrX%QN3R$+4qxsD15f#gBjV9;eS-p{`vl zd@3wOQRMfyn$%3{XnC#bxBw2TfwcY^aDkiG*Z9$o-s1C%>pb)IW9-@8iO*i`XmtyA zE=X-{(6qa4_U&Jy6uD`;H)eai&uB2=z4sS*;N%h*MW>^=?a(6E*F49M&gG4?EHz(x z_%I&gXC|Lhr>lA9n@4!yf!)0L@`tRhiN-%w+iLVwH@Wt?8al04Mvy|tV@`i9C?)(JZmn%?uIny5%hw;63I9Kkx4-Wg@&2G-1ff)BY5US+h4&Vk>Een|)L zfK3Az?>4hy2>d}!j!JqP!)p;8Ap;@)>3_`HCED}I`?qo(Rs>n2z}T4RAeNz zr@o=NBHa#ET{SXUDj+h^?v`0L$ww{)H8DwR!~$)k1jaTQ6M15cg2`~4z3T~UZ?vH> zP}K;LS4+<|@}-I4qJ*nYM_Z+3bC8mns;X(}NE09%bK>rkELg+ty@tbwckuD)HG=l& z^yOEIZl{GU97U_3D2jZlP4%h6!5AOnMzOi+dFjQAy!z@{wzkL2@0iV-`Dj#ASG5dZ zr>x_$SIx|59)9#TE?ih=b*qo6pjDLId1{G6N7`5eqoL>JmoM?bnd|s=$?tsq4({61 zl1_#=gKM=6JC+yt-tXSSBVXKwRxs=be)^Noc<=p-3e!C(=A9%mRn z1gw?Vf3Os$KnEvi0dkWXl6s8bz1V|MSG71DVo6V!1|fQ;pna12PMeG1E_}kZdoMg2 zrpUlP6K#F6^SY2v2x+rQ0U#~5wb;rhJ<*~;COVB3pkCZOb@TeNjaZ83t(A>V-{RL! zjbN!Z)>RX1QG$ud*p@AFZKfCVfY#9k<#dBqDa)x1SsAS9pxRy9WtXCiuXoUQY;BL@ zgQTOR7&YS1^s=ak%Z^0N_{iGYX0gkKIW~rE)i-+s8z1GPP48wL`*^4Zc8&i+rioj zV+#mX(@_^7)HU0i0|+|^D6Zca^2VFz*xYhBG~UQuEWpj11BRmsQRnDEcz{N|SGq+=&_Dl;$bOY_*yT~7W?@kUZ#AkVXyXHUr;5WSW z&UsF5T=>i5(G0I;$+n5ug(cGK8sAykzh2Qv~8$hrfyL z5o;whliKCbc9c$qC0NJJCZ_LY&Z`9vZ|pxPgG&Tvg-tfp)K8#Ov31(rgj}hohQ+m; z0(kkjkfnye^n+DUQ|WV+WvJwKNK#UAV5P2)I5UW@g^1BmnWgK+S>=2!z&%ay@`0G? zk|QgdVmy7+&S#RQ4^62`Q|{C>bE*7cOV^t*nUYCQC^TiOL#H*zXxx))aeRPX9Zx*7UB!L(9^>q%L;Agv z6DRj_+p(o6dGz{RymT%0ucQasIZ5p{?Gn~nDPW2Nz)P2|G8|R}4Ey%a@%UHn;Ya`F z6GEXmc6^z+Su3}yC@&~x=BnfQU!TRh!#wzfB?=116^GCtez4BkmS^9-T`Vjuu-)r38jQGl^(NJ5$k|VC(0^z* z3v-&iyAA*7`^PzQ$1J<|E^+4_i*#Br8V42@OCdc3(6G2;mb*_K;cHLq=Fp)fmgeK) zesitn&DSq*bA20s?h5BF+{RtE&!ZKbJh{Ni&QlD>Lk^1VIsl)Z>GR%uXXtHjP;E}Q za{U@kFhpq0#%%|A>haq+FyBTgn2bF)uHWRw^&3nkqj+jgn}fTx&LJP@q&TXoCYi3C zT1Clam1=5@m`1kCA}aZjsr@C(N69c`G6`Hy;Y`$&!z;*PSRAHaH%O8sjVqNi9a<%` zEV=GNO&9HD>0M{nR=f@A+|Q9@bCnbEA$PrKFGe*@#yq8&?yL$ZiOaZ|k$ihz^aRvC1s8ArMaPMs=}HfXfQ*D+nHq>Lqq zPg+SuX7FH$B|E6t3h2XBcluOKcyj#cd>Tj_b;NbosoOXuTqQ{1YPlWxKoJ7t$vT_s z*I2tYChRI$**V8Q`tDP_`@vOo(c+!g-{iwjZbS>6d}pjxeC4s*>2y}Oa=phFAKl0P z-3670_0=Am+dZnf%7kL8VwlR+E~DI?^Ni~%b|PC%Z?MYsE8E<0q)l1ElTRICc43xb z<#_Pn-OO~;n8~06Y;JCF{?jdv?OUPURy_SXM_Al7%V<<{@BO=3S+RsVIXuSnH@6rL zJuJcAJtcqiFYe>B3tQ|zJkOzh?Zy=CJ-z;b-@bW?{d*76DHLCQdM9_>y}v_#8-E5`Py-Q_Nx!Le0jil1U{+Y@R0@n=wF^9NbTxi=-F6v{OGUWW_j;2UwLdFWh+q_;PyM0*mqX@HA`!2uy)d!46hAUUr`12pW!SDU^PH=(1Vv#Z@ix_i#_Lnu zjcZrM+Qpy=#>=%a4&Ibp6$=}Qr`|asI_fEanPAk7L05IQxF%;4QlqJ)_ERYt6w|n? za!&_-YKSiLi?ktm2^=X+bCD$5^}Mj{^AKGmsCcUpkAYZi-Bh3_a<^12&e5u2%)~iC zP3>w*0V&20HWjfXUQJ`|OI_%xs&`zCF<8!>{fuAz^mPvJe3adLN}l}cJ|28zFLe#O z7vb!=pHYoQSuU#`m_qT`3RRXYrOW`kEp8&>+9RR{L)2Qj~`^u&Vqx7+8jL8#sh;vK$i+DU-(|X&#!** zAqRICoVas0%Zr+$M_M@_(CfjcpRDnV=icG^>N-9cu3Wjv-~82ww158uC+?VGW>$0W zsW}4B8w84O{AO3)(Hr&XuMhdrkKd)!x`*Q@mRX)t?A}?#O}B;*KiuZ;|NbLBK7E-< zU0{^p9s&${p7pgJ*ROB!t6$Bsygbi=y>UqV(8K$A{f*PS{@Q7F%xj+d+EI4zGIYdn z&>seF+!(TNZ@#Xj4=7$6On0wzh_R^x;ije(fX9U%JYuH^J4KI>1Y>e8T7FxA@W*k8$_iOYB&f z1w7lm5ubdt$_p>N&*iJD3`b*X7j5`Kv~FV!$~jg)zro-B`OBO=zCu|jPJeuv8>_20 zS7(=0lCB%ziE1bnO_6jwE&C+dgq-9yVsLTJi9I{feN>(`+yX892gP@J(+J@oq~+DE zUGt%lp3{6GmNq$o((1fnHR?wLYGg{GQB&?HFU_4H_*?Kz)4Y4PXQb)YOPO8MOASJ#t>6!95dnFls1wd3iy z<2oW%Kuu|IfobPcB=u??b@YfkPV!-X3$w15t|kpRvuS%vj}79nR?_}uB#@}4+P#d@ zGPh|E0@;R_Jl6?Dl5tUCYKWzBQ({mdBgqX@52-e?sl;Jf@?O`eVa%x9@@1`w{b1f> zRbbHD>O7xc>~rzTI*&XU0eq9n z^ZvWn`RU)k&c`30VLYtkgDPf(+KuULud{w_oz-g{KEE>L%9S;o6U2;<8}zlSYy9i~ z>pA}5pFYC<_wAZ%7b6HokzcTh^6HSU){x4$y8}-g@&Qqy7+AP5A80W&Y$(Mx46uD33g_kGXlv zWaPMfvBwK9eaL4QE>TYc6Q_CU#WT#dEv;6`%dehcd$UjFJ#W16DVv*{JaFm=CMe!~ z>pW{~eTI_}fBWON`S`;%jvn2~($YNmz=hARbMeYeb}Y_8(47732E9o|ueZjYy^Aa@ z%!m#h4`@~8sA^AL)vVs=@xlA&8P%Tamv6Fb&jPk6XqTldgb}$>O(tw_4OqXv$){&8 zGaQYgoqMy#uYUO+FTMN;3k!2}+g)ns*x20U#_B4)-Vo<%=`PW1Zmsc`fAJDO`(=lv zZkKUYG3X5$4JT|32293PJk6?rsR9=-UgAfqH~7Ub+sw_(P+CiWFre4lX4oGv7!Mgu zDvU0o%qyBdF0L6HlU086{5G%rCSVAR$3uF6o^%4jJfxfAoy>k?CM(z>f~T3#+u-qNJ_WjP;`zAG^mxVfRF&P)3C^t3khPc zRn$3k-k4&hOYx>IO;DT!l;L~Dtpau>OJV&d_dOy*Ep%Un9%23&>99~5L%V10dqjm=Gl!y%JN^SQ+iY(rUgX?NQk+P^}7&}VgRoyoYC4lRR|5~_B) zMW@?l-=1BpEYC8j9Xt2$;$Qspd)T|I`S86h{`Y_N8!ld1$H{Hc7A12t9affS>2xd` zn|-d|yh(r9r><&hXQ_Rl)3FrR(C&8Fy|ROrwQTo>jO$74K=+>UctmelW$#kr2LurRm4T(>Q5u!>1tW3;BQCBxx>{tBuX1j?D8SbFp8F@)o!skJ0GE;#Qad@DsX|?Rn*ml$#8S_`- zcpu{bT~8!%=cH&mZY(}{xz)rCrLHFp+Qj)B{ELE4Y*2K5OkEABs{!6kh@Uk1Bp*>G zb>6h52^CG>jhFvEwQqV^n2mVARPF-F`1c$SCoI-O3ej?m=B&cejbB5RG5t_2}j$9Xn_Y_eKqC23Mu86j2J=r}W4V|{QD zyB#z}HKjz;J;3QpHq45_Tpz7^H^MW#Vqg zZ)lhb4W@$oW@xSQX@dk!56!G2xubk?N$9DL^QjH(ASHaAzV#UWx{;hbVp#S@wL zH6MJ^i|vJlVQX_;Q1fwc@9LUKRb~HqT~`#gAb6Ne#!UQ}kIrm0PBSq<_z*4G#zQ?( zY*iCR!!Z}m7nJP|rnAUs6g_%g5H@u+p?2|$UR4vu;~rbv>yd06yjXv$m>VJH3kHKB zRx2jsF@vohMPYGCYEud$hlg?PapF3ubcLIYS;ym5bkZmfuCA%-34vJJ9{?WaJx%q6{ly1#n;x z2I5*0w~f}Qag7QS2BYnGBbJ$-)`n7^52vCgJ)U^R!x4kcKFUO?AW@us2u$20SLvb@ zVeDchRkErJ>@UaboRd?&45_`Q@)gF$oY7=b=fhSS0>>e>$9s{o-GtgzG3RA9%6ozH zN!5dkc1|Ot=(-lE$;Fw$WF)eaX#YyS!%7>eDhSj*_TK0a;hcs*Rn_=VQB|Y3`1%;; z^int%!+Bz;5kf$cbn3d!5X3Z-4cg}(l+;cbovtb`#8Wm-srYu1|B1aNsj%vnbhw~R z(nLnd979Q95NX}a^W3DwWa%k^3k7gERl}XIDPAP5=#bJGMHJUqk-kJlGAYU+-ABm* z8l1SVR5VA;}d)BA(AhmO7$@PHDz3b|hzIGeSSCl~inkKCv4g#ZN7es_8 z8d;3Jboa(i-T37VIqVlJY--tL$~-~Ne8L0xDN?0WClbVEFy*51scl2x zXSykuPj!^m+Wb(F+~7XskfAZMfJy~ytl~}Kou+pnIUaIHu^<$-BMwF0ZJ{3iYs~!D*Z)y&sAXp|Yp7=uTY1tAr(X?#bz^i|hYevhdflSyna zOd4}dtapvR_$cuiYp5qRUU{nV1g|25C^)2tCU%H;C3b8TkvcZZ4Dn~=jskhVT`KAd zFzOEoI!sZMBetZjE4&ZXB45UIjw6IhoI-JNtOQr>l}iLXanwOoPnt3`9T{+X>SU^e zj@hajyozm)sZyZ!E}r~?qn6Jk=xEyT-s7y6LNqV*YlV^b-NkgI+!j57ItSbcvcRMR zk5n!vrd=ErMlQjYdkeRVKst_D5A`W^7$Z&00PqtHy%1-=S7ON z+0+u6x-~KhBR+SER8lupN`=%sOwCj9IjWHlscCo}NO`uDyHK}aPtxL?8xB*2vq;Qd zd%4X{r5H&Ac{Sy%3R9Ghl!$K_V6~LWCCrV-fp1z=Lmmo906QqLJy&RnYy{LE{yS(GFio|Kct#~3Tus0d6mx}ilXtuWYl%8;x@VWWiOBwJz% zgL1VjsAM~sLgjAXXTsHqJ~cGi@ZOcvcRHE0h$)uhJ7CPBuONq%rIjvR4U z=!80sr=g$}HkBqxPkXA1jddapg>A0PMo{?1;*wJVZE z%Qf7)0II;dT7sVGbWmfpr4pu%a}EUEjF1FWh{r4~^PYUtAcE7<>B-pm-}<0rQ=dKOD1mbDb-fx0o$vxO}S> zALAsIAJFmiEPc|>dz7)lNDI_KQHO~jzT@HB2T$$eaY0#we0O9qc2vY6y%zs`EksqF zH^ykWHet#Jp22d6L=qRG*ytuF*yst28(c`W1F|6yT{tPZu9U`A@e7$)Nv`(Ph;hUz ztd-$th%ipARBQwFE+agIJU)fziLXcg7F<=wFTHcwspxzZ#wf=|z<6Y_1G-p9o?-fm0~s9_o?r$b^01c&_jkl9cP)sY9y3AZ8a zB|R0yBakT=ND$V!KoFR!P8+Fg5I!~kO$~*;3r#(C?kJ+6c}ustDPkYdEprG`m95HI z@3c`g9U(sUa)fY;tve6P)fD-hh+j$dvP$~ebMieOzO}egK?RY>bDBL$l&OZw`x+zU z^r+%DH&`Ehbf0QXPzEE`=*9sXZ#-5P)OCecO|g}C@wRG~S9bg_jg>m&!H<84#(i^`WVjjJGx& z>7LSqqT~u6o5P}m&DbLEE+Irhau9tyK6mjHonD)k6nzi}Rn$hqlvBgyKd000kX>Ok z&YqBb>9T>!hU^j;(?=Y_+S)e1edPjg{PsF;zIBeZjV(r#O5_-w%?~-GDNwpdG4(PB z67&pB6>-4dii=4hrzwp%vC9PX5H()aIOJ3UbEX~{3D70`qpy9oSv%*lYbO5wwLDW= zf}5#4Oj5=Q6t>|IO0TtV^gT4n#%z~Plvk;m_VF`K zi(wFFW0<1IYN5v_)*QK`Cs|=t{ywLC2Ko6^=%6TNxJ_vK%?U0yfTlWy;5`o4WI04C zOEF$6HJ|1ZW8ArQ#`EndFNETZ;GhM&m_5rgsBEZKjp>>8HFy@ zvC@Ckz+=@dS(9nh5>5AFzOBVGr_RQoH2Y1NG^OGOsZD{gCiAuOp?S(EkYy|H#RY4c zJ`j=H>pBkqwFD>Ajmj7kD{U1ahTih;qNJVB~C7tZP=zN7sgLSQ=FC6Jx)~bvHd~I$)wQFr83z9YrJ@ptgGoZBLrkhr3QZzb# zf#R1=H?9p?=wpAeX|U`P;;idRPFoR<=tS{I^PqIY^DYIMG} z;%TIwj9J@QV=&yt>S+IUzLv!)S)tPgLbLb@X54udu_-~zm_X)jK14~VFv4ClS#oI# zco6oH0t1z+@^#*ac!NmRS6|0^0q;fo1__M{5kYcJQ68C21-o2IR#+ptzdQ%NM(Oza z3acgL8i9$yx%}K!l2lh(Gx2fgSZYJ<3qb9FAVBe*E(ikT=4x{)SZZfZ4lQq5& zWo7T{h&~C;_b$oRt+LGgvAh;Sl&})!+{*`@>LC*o%4m~)##R>s_lkDbbiy%mDz9DL zFdDL?IC4OYy>j@2-9zI$o95$ zOGv0^r47wZQ)T{^l46>W2W`3;P6j2g$*GhpQGPJiP}TAwCE9PAai)b=++Wtpyh8g1 z{wNMNw8i^aOcgJiN}TU<(h82kwy52td0^$s5Cn5?Qg^d%_+lY=tStztDZo)8-{oN< zx}lubilLAIK#NnOalC0Axj$NKCRIEHq(vj89E{TPOhwrt*%7sFQvE0$Q~jDqb5??- z&R=pL)ph5nrCPKW!U0}nDP`kYUB`_-$na80fK)3JT}+a=^^zlsR0IRKXsr~z-Gw;k zNt(Dy^#LLA3*^g19P(pi^OmwPEz(M$7B^lx@Wjn1O71=rRTHc;N|KIRzK>o8u2Git zqEW}!8=RNhS3DILN>gKnx+;>2d~YjPQ|KbfpQHh7eBFVDLT#+2640PE2IFP(3|@k2 zmdd9=Fgf4gUC0VWT<{`m&DBo6J1Vu>#o0&Y@KTv-;u}j%6>n|I5|qO!*Yu4m0sAEY zVKTvl=*kL27ML|ASIlZ{veX)JM-k{6SE4wZNQwh8OIPap^%wWY2glDDs7P-a&d6Q!(oG9g49?j1^@x0dS5rz;?%38P2T^SL>or=T#GT=T&u@N)83Y8nSZ*iInLC zC|BchiB3L@M2uvdeK?PnMK;>4i-@XE?mi!%-MWq(H!pIgu|>=$`3Rcya+@`Y6p+Zv zQwR%{WFRT`(c+I!#)H)TqDeC{rm+7a2a_rhl$b&qmkvP*P_*f&F`3V%q}fu~^aM&d ztp}~C(@Cg}jBmu1pg0qx1cI%ccgK{4HnBWbA(RTc-WDU86 zfuCfe)T?30&Le-z`>FFOJ+@oNdTk|Fz;FvVid)v52uJnJu$so@q*UYHQq$E!X`@l; z;2AP*r5UIT&{Y%%FP7NYsH*dg!pki(lYgb;Ddd`NosC2BAR6+F5IHoU9L`nIvaK76 zMy%~khb9BQAqA(yNs?IO{JR!vw{3?13UOg6lRI@uBu9~le2Ax?AgS<(GLP5r5Im+Z z;NuW6(;AZ|?t@%?$$+p_m_iQnGzIkO1!s_9X38){q|{mKZ8XP-{!U%&K^9=9ZX@aM zL)=wbdPqoQ&FoN6;=baRAy=eYrrQ%}RmA&HK3QjP-IQfmuJ9WdD*YTgJw>?T&mZB`hnPS8qtUQCdge)pOFJ9&vO6h1f&oZdO zYD++4rRg#1;PM_BnAA?@L6YAQ=}t?5PY@X_$owUwrn=PeAQc5huKk&WkgA%eB$Z@Q zO^2bfY@@uDLNqVdVu$gXib~#%bebW{X-LUY)cRH45Nknxq~y1>$7_O3YRXOV{>~f?<@KDiBMLbzwg%Q9MDa-1RvZK;E+7e&HpPW_7GD3>%X2m zkWw5!3dua&|H*KWN+%z=vIC*w$IzUK{Ec3EVGpF@3H%kG=Ty*Ds1}mnYpO}D& ztgUZ}Crr1yItW&tvx-pb9DYceoZu|uWPaFUROk)uMhDLC&lJ11FRlu&;|ax8>^ zS59)vBb`>e6Yo zSlZEHe!j)*>C>F&9 zS%+9%YIH%TJIm5ihnd+Ht+GvFO_a;T#%(l=M+3HcJ+9nX$2-TQp5zTSnh%6DAd9OB z(lOziA*D63yWKmOo#`^$Dd?7pYim7vgGt8jr1Wj74)xPtY}ZJtlez+BDT?^TwuNDF zah{z!7nz@JGntH8TVH4O#&)Ea4o2}A)^Zai4ts0g`0~k{ZLCCaUTy?RVTcE`AUA@P zhbrWjNxn6>j1tnu&~CRla%c~C9N$ZKp+h|y^X8iuIDPgqjv9l_>?6eKV{`%DWpH5b zhw#!bF0~BKMcb>D6Rr_S%}MH5+j!VXo3H`IrCwS6GITnwlO;Q{Agt|fuga>>U_%&Q{g?O`3Lpd(eM+$t2(;EsTsP7=^2Al(Movd-3DfIPbB>V&znjvJJTyKF=Le=&cDn`6<9+-jTD%+cx3256wkw>ak-c_`OJrVK>Pg)oI;_F;;M>Eni>rfPUKG>e~T_#Tyq zp{elA>IvG!Q?r+uNULlUl%^;;EG_Nf+uwbRhaXvpn&{F~0Y^cQH3tL_9o!!C=ITzq!D_`?Fs%9F3_aUPPegloV~&UKm4BI@3l= zH-W~|9H(&rM};?iBH`dVR#7|L{#-d*eK691m$a zR|bbzd+eiw#+c@oleuOpE@9oy0uO%SZod27qwHL^{MDa-z~BD-ZNj+9k51Iz>!|l9 z0)c7d!sIMd#-OdC(=NIDt^uiF2Ntt9^d-*Prn6E1y!=fk`dEJ6W8w zHCe2a4lh%aDZN{qQ`%^hi4Z#<;%8yC+`{s$soCh`?Od^A#~k1J-raol$vyFu=;9&g z+Rbe?wzep3f%moCy1f*+*?i#AMkJHmlo$VA?*h6kv%$!Sl|0ThBq)||y3Om0w%jNK zdlwh1)Vq_K360D^lr}6cE%D{AoaCX0N^}6_26VeEM%5U!;>OKwZmf>!&dgA>pk<(t zL)FSkhxvthY*C^nvEa%W6|aa>?#fKA$Qy;Yu(T0VN#kxyN=GbnikAKC;L2_(F%TsL zlCgqMAqSY#CZtp7E;&nc(NvhiEh(9=2y@CeLBiBW6nujUabn|5n}L;$Jpaxq3+0=6 zby8(~HnYexr1X?b?wU+VP*M~qH-9n3BJVp26!ja6s4P0pHx7wBFb~O@VY2SqkatzaBzqr+ ziC*;e=!N$UGG6IyRWzoIGF6D$xiJOaD{N5!midJh{(rLGtl6^UJkNVFbFIDinWs86 z&plP4X8=85;sB8#L5e|-c%P=s$}hpezo$pQgU5=e;v ziHQQxXaGH+8(m%1Lrq;>5K2nT&tj~270Ko&)R!ue#84d@AEG_&Zl1- zaB$V}&2RmLdb&m37$ODEl~`M_Fu%kv|I(BE{eSp0hYyuBb!0M;1!J!_%kg9D%rDF{ zoYq29iOCdJ=p0q#1;#j{HF(wU_sW4(?}-+wO`3G7t>M&~$oio<*47u8o44K1Hclp> zIGsNB2YqH{1w-N*M{ELxEtHAoBo0_{*D-oQg>(qWNwPRQGsh$6PV?kbYs~iG@kbBv z&HrrhjnOnfRovP3d7GFd!M4+O7Zhcm#f5oZdhs}a@9%wzLx)Nd+s+!x%F+V=vpM#3smv35AmgR`0VEnNe(^IM9t2X%*+m`>YC|fg0lsME%V4G&EI?aC702weqVKz>&k-K%i)v& zqVil!UiN3qPY@|eXA_GJx-iD!tCsT&%Shw7oJ(7RH0f?sCmuG;agQ+&?nhb6n zC7erH7Ba4^J!M(y$X70`BDLC$j-aAX8f~MqV|9cYCy8{yYe%87TV=HvCv94aKTK6g zaz`gkyhJu7gmUsR#H>0<`EEiXPEO5#B_DaZZ++d2T1}@2MBpsxx4Mjs~pNPE@K;Z#A{hK;=FE-@o}WA6!1m z@e|AR2dZW=Fh6TKaJb;vPpnG5`t3dLYz!$}NxukG(PNyWv^}t5N)=SJRFJ-@^^2n8 z9E9sr+JZZGws`Ax$IMK@)$5yzr*4(Bz=mHB1mf!xJC%F0Xm^Uxnpt6+|22Ri|O;h8eeijg%htORc zf)6x03L!druCJpi^*b0#QwK1fpqwFZL?t4CT2Kc)A6lSpVxz}goP>;Aj52O?wzI#9x%0MqK3dym zEhTPXD(;giHI4DRwt|q-*^}Tw_ZKk)4Tp=l$dHksyuY>Lr;>`xclOpk>TA{ux9Am4 zlvWAIMXIO^tv6M8;R23Ia9J4XB?Y(KoKPjTnsw^~dUW~C=;|X-g zx!Q%KQIr^g()>-Tp5#|Zhqsyvq(yw9exc4@8xfaIhm zU}9SIhiq^&Mx>%4#R2DCWpecftSPdy9-YC?&#rRl&?@uG^OR+eAN}AhF8mz!wjZ#) zJHi%@h@)_Q=I2&9aPS}}Pak3Z@FInUonghL_ik{1YmaK|xqWAwt<61RC~y{fMM=LX znVp}PcvlPi!>P>Sdx7D&#!f1*h65`zOhygE$%ICNj1?bI6Db(<1}rZQ7>*i-`$N9} zy`SR>OVR5wGniq}D_B_R@kjsQli4A@ac7VHy|HvS{TX^Cl%->3ahcvgX3)c_01Rh( zk-cHf=Ju{;rI*al_c?laou&B|n&E^W{^&NLneg$gZTiK4+SW9UbOo*)FhA=!w7x>G zgzdc=?_qUmKz|_5{`TEn#&u+7W`+anB1E{e(eUSAe~-&o?sMei1OCxJe2$ar1xJqb zdHU&NT)VkTQ92GR&*5y%jhkC+ZtXIuJ%SX;sM3Z|1O4<76>=sOk`OD?{5liM4c6a-vhnV;|TYrpX{FMRrdWWaRdU|nIe zDYW13G21UWeE1;8kFRjx@G1+lJ%;;2;_Vfd*+Gwsmu}J+%c&FVtS?X~x~y)op} z(Sxk5&oUYB^Zp0-ICuUCPdssgUSZf9*38Zn%ntgz_0~tcfAs-RoIA#Wm3i=ix8AtS zi8G75@WP`MJ;Q}}H~Ho_-{ccdp5gcY?&BOdSkNEHYQ}jO4I@AK(KY_hfB82I$0_?; zqG+d8WHg>InoKi1M6pw%Qu0wnBo-Sib*xpn)aIAVl2mx6c-R=b4))fQkqyT& z=EZzxlAd?7CZSL=Z%<8;1d(#5*z6cH!l>QwM&aBimOQvL+O^o)+Z zSk(*JEU5E5!5GOhx0E0f-!#&q7li~Bd4Y|I16hhKSq?QS!A#4{j)^7T;qez3FFX`( zZ`61d6Hw9GPMDQmXc_?<4HvobDw*bKmG@wW?`vRU}J#TaOvHT_<#Roll}27 z-Zc5)`%I>hdmCe}TpMz1eFr+vbI{{EHn-5?$*rBmJ|;D-Ji|!Z~y#ln!2WNhNqr6!Kp_U^2nuosEH!b(Ce2N z>v`^e5bSFyc3V<8h80bc6=J z@~_|K@pCJ@{F!wX!N@3*2!p{Ib}8_OzyBx)mSoYhzSdC8SzdT)4r`%5DERL83yMAr z<_aMT_n>E?1QzGvEhAUKe9OUKHMNdyG^adtBNYQ>>5_Q_oWnPr5m84a zP^vjBW=3($Bcznk(Ru*n-LNh#i_j=tlmA;xYi3FF!<03iLu6Fh5E8>C>A@p;snj0& zCL@(wPf1$oJIR69EaaRApeymK*A7kJ;yUoA7O5`AOP>bx6OxF zH}H)JBGwKs@P~i+NnUz!ok71H&m2E7$gG{4x0htuIu=)X*`H;8v1HIUlunkuCOD2B zTcRk85|Cvy)Gzy-IJ?fVV||=~Fa7$_>^lenrWS_7Y3uLFS>0pJu_+ntPPlaG2A})X z5*8?X1^c6#;V7`MBtyxQr`A|_^CrG_P&f`BUgqF>NnzpM-8~5cJG9Ifzw`*d`5UL` zm-60tA8>`?;Cd;04G+speHP~ym`=y6tPJ@4XHIhFL>~)m?|ByI9DO}EdZp;_ZDDxw z{Bi#7fAutvo|R`;*O6)CDN0Mfv^l?hex{_t$ZiT)JJ4rlxzC9svhK$CZ@lpVqiVvj6RVs!vB;C>S2(lo zvQ&9yFhgN1Q(f9dA4K}?8|r$Ro%fZ8@pMYv)WUtTMwCvWRn~cuPv`h6ssY&#*nl`mj=aVaZvL6 zBp(zV#8EZ|>RFcHjSMvbD7u)m_vKhszWQQ{=%oQ9?7TE^oA3P+%{WmYd#Rh=o6KGbrMcV>7(5sg`U z@4fCa)I8(Vtd+Z*tbX{xgohgVY8|RE|jYEl&E1M~DG_Q>9I#H4-j@EPm zD+y?(NDyOjwoHED&OFoP2+i0@Qx-**&2D!Q7^G#E(s|X)n3!2ULRC-r&iCG=v6e^9 zA7yrNhL=8dh*Jmq6oqAWc80PPSN7g;hd=wbKj7}>vpn>sER3Gw1m7@0{hps$)8d+`P5Jc-nC6#0o15q68{SM_H;{*ucul zJVy>Ma{kd(R_Em6UtgbRb#+b`o3OJ}vAegQgL$IPedm{YJpIf8X8MM`t-!^Lw;2sR zM9cEp0>AeM&++n02kDovyWMd0`X;?z$(b|D%n#&CFerQUdeTkq?oIgWpT5mw=MJ&H zHp|(^mU!y?9DM`5o}*VvRUe!Xa0i0{gMN_@wtiu_d}YLicW<-5Uojomy!OU5_D7cC zaHL}d!_lK_EY8jHpqerm6s)Z-<8B3oihtwYkh=D~__?!u@mEeVDCMGf`|TYrU%AWC6DxfB`2)JEqcK1E$KF#r*v9oLOINrwhSL5o(<(f232GR+Z**rL$kIG` zp=JbCp~|4dClr1V>Vnr@NLh$is&2e$3++SCEsgyPSC-jWmaHjJ7mcoH4KCh`jH@il zY-mH>*@Dqd@W7Iyl$m)xbdxY9goh#^BZ3O<22%-@l^p(*jaEjuF28XmZ=<6~_S0Kr+9cmq7zI$depem|?g-<=fwSi}x?zV>FtuH<}QFV`ebJ6VDuFZB;Je_ut>*D_?z$ zy;0=lFMNW({iWlq&dD?NzM`H=j>6(n$@+fB}N6YX0?lZjn+$w#i zrc?EGG*!(9A7117jaw`(Ec4PA9_P`sbMy!3zKkq81VO83UKw!xqnfXM?JeGW?_-AJ z2~`znYOrpW-JM~MGkx^2d0u+y3>Pll=IHSyKKt^~tU&WV;G2r~F5PEi<2WaemMkw9 zJo?xg?_b{J>8B5I_G}*qZ0!WDU)uvyaA1AF+^ifJcked*)n8ra<9pjIukEm~w!rg` z3>0D+v4!K%(N&&&`Y`izvMj#%?gQ@J8)XzlQXZ9MNv~hBusEN0Bp-h~C$iN9UYMp14G=2r=XF)4tZ&HYCp~ z?9kFOxC||{)+@n76snDm5w)9%NIZZVXra_s;V7Jt5Yq1xf~Rz)7RMgj)Kv~6$#1YK za6}zt9vaEWjwGlv)pStF;BBsd@ z#-tRms-9|m%!Eo3|HUMEqG>${ogkeU13}%FnfPj@Yst$u{ZJb1Ao4g(51`Q5rL@1d z_&ki}qYH*ZSw=c03|>@dRUO&c9y1yRb!=<5;#{lV2olh=KOC{UFR6FBD`Pwvv$M0y zXt+-`ozm15MNbyc#=!1Q&5wWlA@}c%8EqTh_~|XK-5%!m#Tv`CYy0dBBfUNxJGn$K zb=I{UIyB1*FCJxSQL3BG2SW+&(UD+j4U0=N{OYeh&f22Ed$@XShrPYAj3$HSz`+&z zeYwo;-ktF3tC!i{8nL%i^Uj6)?2UX5*f9}`?3%9`kH_o}M--;Wx$;S5?u@nphqJY_ugxvKU7u3vq?<&XC9!0J+uwY52>b)->G?Nd*zar(%- zzPB}RzIl`DAMZninZDtPGY3Tys#DzKNBf*TAwPfPp6Bw_d(>4Wz9-*^oZgE(J*PAn z$wP6@=EE-Ao(GcRcI zNMwj%V%|X;ah=ETv}~UiuM_iSk)|gy_V#L2&RU&z>OpJPO*Cd%h$q2-$sJj;KZsGX zlhXQ7=b{HtjHDG!0*{&~omg567`^a}X8TwxQPo5!ouyZnEwNkE9qXo&F0aw=rRKP& zPRwE`y77u~Fp2@E@eM^$Xberv445pJ(-~x8i)^SPTKTXCVWu=q)%qq72XnB=R}pe0 z;C<5~heMkzDBK~NvC^{hu##J%)kR(xrC3ZW*z?eMjEgyQIBBcJB$SMY3Me2oOXHBy6L1KDsI3@ zJf|_T0B@RUyI0cJ>>E#2)zo!OQ;XtaercZh8BrMx_osaH(H(~SQ}v-J-zl~2q2|h! zZ8o+Jv9?%p?x`b$A~LDvSuD(2o_*#ZGXt1T;Nx5O8BIo6C1;%B#OXDT9TVf{y}LE9 zy>^ZL(S*7d56i;*EWJVw!=0TmckXR58V8J#_*iki3*JUnV?n3`F;X}&_)V)S!AxaI z8CpnPQVKSW&!f+}Zm6r8>9nGrN=4g*DbWUQ-Q48QzV>r`eV(VDTwr#_GMJeawOs?Q z5VVSKqQtRI8{U2QHox|3C+RO5&OSQJ`6m~7>dAvTa)J*&xWo4L9;<7!nB=aGFdj{_ zFQt`o>&V4fPMlujrI(L0t9zmS;gnOS=9pRNQ}MKKIgbHntsunf4!4RgG_i z1=cHi%0z2NPLok!I+6pyIyin%_9o*glzkW!(AVF0<6g!6yEUWzI*UD)X65^_KY?-} zf3MV0%Y*wPZr|Rdsw=9h%DcY0s%dJUR|eu7k4ogueL<#b*Hw5Zp262X7jZ%i)|M~^ zoD~sFs``Bhc?XhQ=3Ya?(%-YLyF(3G-Q+u+Nh_F+9Z#&uJChWime^(zACQXC*vUd~ zy&mc)$vX{W3`Jp;PAxL!6wVbADqiX6Ax~&bX953TI^u)6%G2fIjRrxPHr>@&mkG#C z)955ANMN0bMBnah0~Mqw+jMKw6+Wp7E4d;QL&^ZQC@D6hDpkyg;nyZ<7({+$(IYT2e$zvb}lQAY;#(mIaz@%PihZ!cP ziIJ$BxpGnIUqT3|7TY)4_94jL^alR*YMNHMRA$i|W zw`W~fq7=)X0V7+n;BhvnIws_Vu^0kXRpDcxX>=6nwdL%%>eJ#U21zi?hm1bf2OHaL z?M{J`wPiWrZrqu$H>vo3;CuJwCee zfV!%w8z)hvwiQZ>>B{1)j04JEFYB(I(?BN8R!O*RXDx;6(IIIoxmV}gNbIt8El;TN zUh@os?yh9dX1rlE+UMtQUgyF69lrGIk8|wE5@oMoTunJ~dYKdJ1u?*AG}a5G;_kg2 zKKyWpPd~TJV8-ysV{05aC|~xQH%8pJv59XYb<=?D0stl3JkVgH&%nu4D6Ah|V0qc( zEQ^yz=QwrrOeY3GKE>zG&2sYWX}Ce=i~rwnv|VAxkJ%Fj>=3I>(?_=ADAe z*)0lN)U(G~N3YazNhcQrB{n!$h#yUz*49{6CHWk4>zrl+b=XqELTk^1@{KOQS1iRIaO@5Hw^ejX=`;SVPlV z-cri8#7f#&!nFB7&@dhEy~OG&j8d{g$jO7YXc6EhO6;ri;>aHKXY&Xvbz5kHWAqJ8 zY}A!%@|U|;_OTknN-T&zQjaUDsmKEcGd zl_%FPrc7lHk;!M$J<(w4>AzJ*KG` z$H?^?HDCSOh}D%vR+i>Cdv1mEPp)DN?C*OnUA)EK-Zs-JaO>8DOBe6)+*3>RdWKJ& zU#Ap$3Lm`pfSb2Bsj8aItt~e0Z4%=#jD<624zjv@gT^;3%=hW_Om-!Q7oME!NnCDvZ3bos^DHl}(o|!{)s$}PMQEDr zkx2e6V}cU#L(a#+wgp=Pi%6%C@=#i}RC}S5)tR{q(6MS#KgAp}eu% zwASmsN61d`Arx}2=u-!YEG{zUBO|0up3^l=or9r>k)BdyomCR8gQix8XZn;_?I?Vc zXPE-yDj1C>D&e&cy{H6oOS^@jU-o>NBomc(Lg*VAe83e2(Yj2r$!%S1+r)nA<=y0p`O-)OG7R`OEGrNz3wmXtHv0g+>DmKMt=LljsgR?*is=XJ>c zZ@)-3Mbi3^tDV9Ka@)5#a8hCAL^cr$Y~9;rcV~!YnbnmZpa1-m-1z8cjE0fpCs$cp z?X|Df!em;pyE7%SkiP_bdkr@?_IT>-DuYr+Fuo4TO_FCkKfl12e)AI?Iyk^YxPEzy zx8AG8mNRGO`RvPQ_{O&`V#^+%e)a?lb57xjM$Cz}B{*k% zySkIrvLhF`As0W^8G8L9KYwRL^4g?>OpT%j!=#ze??tRHm`uia-)HsU0lx70Q_Rmw z;qdzF8+>^6hNyjfq^TM{y0OE?cI4PW!^}V))ZQ@g-lf}AqY4`X)9Hu@8$-MozS9#= z4EW_QJipTaR=3>I05FvdoDiz5E#p!PC^94=(TU{IdsG znzOoRiM;-k8#FH*;oQkNy$m8-ni3kmZr@PFMuZ<$M_7$w zwbS?~M8#}J2}?0X4yPv8M3cgd+F78u*rk<>NCzX_t40BGT1D$OruAh6(&U3Zqu#B<>I^avvr;Pywnz#F^IynzQ>JuEM6)p{NKo>fN$Luh(g>p> zu#=fEK4zOKgOB$5)bU{O_@$6IhUY6>v;5$MP~aj9Y?NQ-C}=lL{)pL zx`tR_t!H~{pLgDRz|qyi%nji4zjT-rk1S!F;pFid`h|Yp5ie1lmyyZq0aZ0ja2G-o z35|!~@l8X&U&=114sFg1P*(!gYwAh{U9o7DKE7slcAhVO@kze?42|L4m9^Cb6loM`&EsnMT03ZNKL_t(mf5}k! zlk^L{sJ2JkySvT)*z>*bUE|oHWnTEyBK-oMeQt@TpIOAJ=OdM9g&r_BZj9L43M|b@ zHq5QtQ$Bp}J||8u5Khct4ead)u3W#zcrxVbmAl-$bDD((Q3T$)xyOYI_gGn-BRsYO zz;I8b+P?Cl_RJl>8t&4?ofg(H;E?g_C!ambKl&%X#L{9(zm%OydY=8l@YGW)JpRNA zzw*nc`1JF~`Jevh|3wb20MWV>x7jiIDFU64qK6Xm0$lsNt#ZnIOowaR0bml9OaEOScml|Nczp0$9r=`2haNUtn~W2KtF7`)8j ztzHufq_Y&dJZV|{khH@Hay#QmtU%XPubuGfDd*fK+%i$%ig^ro7%ApOTml>pO zRWe?|UPY04RO0qF4}r*%bF)n}Y?)9_$6<{c7SnD>+^$X)UAhnHI9Ef66!~=~rWX0Y zH-!_ZqN>!CUM|o@l?)r_2+8agh2a%-xg8q0K%keyzrTH1k{9m z{^HNyU}?eeE5CY@m1V~#Ke^1aEMq&fb&JKCfS*pe{N4t)Z=Gb}Opgbf6E0l5$<2Ee zqy2`sUNRa-E`D&I(PS#Bm!iiDFPvs&O}rQP?@jpWYacSHYJ3y&7_@1ojCM!-$NzAV z#ew0opE=0vtmPAr&2{!7eKW~aeZI8e3Py1eWq0j1p_&Mj_yyup>r^PD}tMqw>Kd-EO_FI{6inQ-&Q z9e(u62Q1D#j>#%}_e=CHY1=rp5=RlZw6Vecu1z13|t=sMAO(lDNlqRuLeT-?q&yi7xkwnUnkrm>dd4QNy+Ev;MKRM|9$9^B4# zf}9ks?3^kLj7U_SmBgzey(vlaon^IT@?^R!lBM2R&0x;cPaeue57DP-lVTnU%^Q|I zL`ZM6?Z|{Pw??{+!nuq+NQ?RyTSivscu%A0%t1rMs#m74F5|;v2ZEMT%|cXNkjCJ4 zMFG}Woa@mqdlb$vKU=n8_qzPGCg5{0&zSw~P3~NNz=unQ2eqv?EZeu>VX;77Hl>{HBH6MkM42d&D+crhq-+D0Uv#| z!QR+%^@B|Yta0b&kXL^E9=rSdR1FM_qh~!gZw%RU$}@aF5+_>(_b<@CuFwl{~o{niapdesw6ik7bK>eUVY z^iSX7;=4yVczBVy`5r|N&Ofon>bzE$bxmw)5k~k%L!9Jiz4hib{@M1BC(j+H8jX4N z_4nA>-K7bZfh*YB-s9i@hfBQq`W;T6S_2nZTJQ0>Pal*$O9Wq6j7B@`O~!oXD?j2J ze>ub4Y>#_)@3XT%<}d%_KCk}pG6vY+-DBhaCRGii(LR6v7rXrEl?(KH1se}G+1s!9 zt8eb|qwl}NOn<;|IOf*f4SXXzDH+Vd()=t>JhH}_6N9XRZJLJAR5bON5IsNp*+s5i zz0AS&bru%pC3p!D;M8?ZRZSW0@3FPD$>xI%>S~N{Y6wAzvKn-!IW38bZDLTeavH^W z@U86DS}{z^WW{ITtLzUI+8Eo(LK3^gUkJX-+2^tKoQF=XM|OzQy46!Ak}Fa-Qg?hL ztvWPyGAWO#gJ0CHB<5JzHY=X2^fSuC68CzqlhaOKSETKak3n!%M$asb*6s2J(}m^= ziL8#g1S*`dviuTWx{RO-i}zK|Kn_t%Evg2V87&?kWK2cMlgMVepn1JQ77bcKrhuQu zrx?g2=_N-!nvQuVjkHWh*B`Y7Y<(mtfkWNhO3uv2885~%(`h#obH-SQQyOqG5IrQ1 z%klIv$3V8?43j$oLyHg(@fWfOl4R4$KXb{`qS$9+EVk%TmNU%HE%E5%$2oO+LFcw6 zOPHhXnS3+FS$cz~Hc+~3@wZY0~I(QJ*vmQ1DDzNG&JJj*3B$b+lWC3j61OXpI*I%}u)fY?=T7pI*Ejk0 z53Vyi>nLXezyEtrlh#SoNkwRaj$#ahvZUXi;mDC?KK1Nj%2Mun9|BcfQ#YQ_1TYQz z`@4Mo>p$VU-@Sk<0(CW}s=a=0ynLzZI!6Nd(D3G4KjX^P>nzUC;C;n(I>y*YJ*{c# zhUn$eNqar-r#yJD$-n=zR~XC;h}2BRLq?-9PIG0vaV#t@@bq)%Id}dryAL*a@6raY zjJ){LQRZgl3+$`N=GI93DU}d@BckH+rQ^X?&2Y5M#Y>>MXfp5XY&Lp=BNF`j+)5KHr7RI4VD zYnSgc8V?D+7DS1MEk9;^YX@r#r4yA9A>d+UTus>@?n+l(PjsG=;#^H0r=;T7c8r8E z!4VA?vWR8!xw0 znlYvp%Q~oHAPWN3G3jG#s!NM}G1zH*WoYMnN zg2)=xt=q&(JA7$pA8gcYZ|yKMJI~SMCBD%`Vi4D_Z>EICGpdGsJlW1(jH)q2Jfq!R z#c4F^kMM*z#d*(8GiGzc=d1=B1vvqZI(P~rAxftnJ;Lw)ol~qW!q(=5yLTUO_}Ci9 z)V;pF?fLM-4aVaM)>_uq=6UW@r+M_uDvv#OfYYb?6u@vC_~3*4Or}$Umm;RFYl^}# zo=lle_f?GGRfQ!w`7}oLIgm#%=385L*x51KHTxX?gDzWr@VcLKOs7+dVnVMNG8p$n zbYY?hN{r;ATi0WGah6~C;u)U*#DIRWOx4JN)$7anpqaw!Ki%Yque{6d{vMNRLiAqp zB|;>|LZ0K%Ug%3JIWcjTEG zGKwb&beqt~*gE+TV#GHA3dg7pdBiI?B9r$vRg#ED13h^Dxl{bcZ+(U*9$)9cvY{wM zeZIfvdHd~~eEZvPu(Pv6T}?EaH&E40KGaMBYR)VrRu#p(sYQfNOn=Nh1L_M zQJ-nr;52I^O%1HoGbD74ePl9j656D(dh&nx6u3ugP?oN=l{%Bc!3f#u9itS$PDfRV zj*&v_M7ZQS-jkSpW+B9moH%9+LWlvI4A&rgrJR^(ItEFj<2?zCLWKY(1`TjF5@G8- z))l%tPe^#7iI@;F;5BN>tk97Q0-gfJ1}8~a3XSQNJ?f?+&`A22nn`3iCvS{ze1?1( zW2ov%;#M&@+tW5PWK#@;)?psCB_!o-#5TW}_>hvRof+F!#;fMW94N1f?qn!@NTJ|; z&YDhKmCU!7Jk*EM5>2M*YDJMQ2mM*8nzfkhIY^a<%Ws5?x|}Pty3kI`dYo|@uw{At zvD5r_|NXD=*yGF0ED-+u`ef{u&?Oyq~q>bqtc%Vk}KF#S|rV9oY|6mNJKo zT@bUPRpaDjGE$eWWm;9N9XLQWMZ%5}c=&DRLk(>(v& z5`&(}CEjEbdF>~6`Rl)Wo0~W9F&gd52sujkVVs2^IDOd()uPE)j$E{X*Is)IUrqR} zFF(mMPp`5#rx64mwzq0tef28e{?6;%x_y_?a9{i>>YGSicwXr0f$L??2hmX1&@1Kp z+=K>WjmS4ciyac_u2Uo06vKpSTCuY&k-8fjyS(xG75>vV{)Ua)o9yikrSLFn5;Tn# zIRUR7LAbq?}y-bPytn!F9y3O?;;wQKMxb30u)iR5cq(TZ~&G)V^% z+gA9hs@jTh*3u~gpLSn%Lnotq2 zQtd_!8Zk~TkYH(i(}Dx7q*SHUwSdoA)L^sTI)MxGrJ?PX*9VGC4tEU&%liXGzXv&t zo6vM-jB*M%J*Z``W-?wS+O8qUxG$b?yTaD-1l`X#h7eD5M4=%H}D_^*D`C_7u z4INg;9gPn-D|-@y2-?tS2w+&3Pt9iR)cAPrHYj&Ut+fr8L9jKeP9|C`siee)LgzVsx|JiX5R zoXB$T-=FZx53lmGpI>2Tcg(c*jHW~G-`(cktv=%&K`rd>R($un@A1m37uesQFq}-O zmD85qV-rOsB9TsFs`RSVGC9grW$O__GE>Dsu!^(^og_HLTgPa?cMzJPA$X@i&meFO z-$*2YE!p4O;#+_96E6Jx3ePoYm!7nyTT$%eQ#@=a;y1dz0aC3cexuS`KtY z<1~$oiwni6ps9Qy%m3kUn>T;<4%e?<=J4TF&Oi1DON+B?ZSQdP`bXTov%!PyT_&SR zCr~No{Xt6g4BbcvI%%9yx-NnQjm6N)>aB6|nqvbrU_3wg;m>&c!iO9pLIp zsYuNGPjVPtdkc{dW$QG#RL_{a1UF=gfX_?#B-BZE1gHCmZnqNhej*{#>0}kTqR5QE zWN?#-Md)0PZTDgFnqJ1cF-LC)yCWWV{38veA$xP5@G`m!ng}xEm9%f#BszR(T_1#0B52MS-pz4GiL29j_ zAMz#B2Gf|%Owq?&kg0k~F)X&gm;&fg_U5ofPmP6zzPY7h+#Q&LvgpwSQG7&OVr>uW z29)J8ra#Z|BZoO~V3Eq#4EMKbswttVm`o?^kM{8aLU8(AJWzF&g`-xF%V021Z!pjL+B`(h)`Lxklc}h=V$h0OZhN2hQlV+TEXJu~*;0Ei zSPPH^n^^lhN)jv3I8#vcdK6_rzqG8c9cFcDk)kZv-q~h6-eWYG;A^iELJJ;vH z>KZ*;u(Q3#cs!)8Ce+OY-vo_UPG&NrxOX8!H@?;+w}5YfSJ9T``|yz4Ty6N#(` zK-Aq>{j8?6};u?>}RMSc-@(@KOrlYl5 zF&QouG6X&BI~A!049yPu3r!+j--s+kbeZX;4#nhBb*YX(dP zA*(uV+ZjQqsSKwoET>EMWZpCR&B>wi8S8GcUOTaqqBVKBXkF1N$&)KJsJ7b{SW{qZ zpQ4<@SowF0qLigpG6+V8aSm%rqA3W`;)))D0$U7lc0e)cQxqj-ugI!4K~*;p8meX@ zb4T0H(%2A%>s7i^eH>l^X>#ECsEd7LC~ZMm=-5feb%pCulnzXwutGYneb8l^bQ|fw zOiF%d=v1oe2eOVPG!#y&HBD)&gY0Ztqhld(DI*FwXDu^jU;ekT*upZcCi?k^nP{wX zbc_Rsh2V7GFqO(w@eV!&t^8!K;jAS#6~V~G5%a7(WOp=1!J7n3w2G~cdGXn3fn*iG z;k6QUirKcd=y;V>1!$dyhsgz7lm*T?oU@cgsR(W<)_f3%r@;3tm1cJ*%C&V)`Cx)S zPqDv}aHq1)qEHHT@_)&`N+5W(>==E>W=tPz9a9HYP0?9pqNuQGYbWU${aie(}YAO+hRv|%D zzR6#BV^At@tttRfI$2yM^+(-Z$T+8!@fwVb6O?Y*R)Es;1*Pu>{eUGMcB(`YW@tn4I^5z)j=8O9~Cw^h_649voBsro@UY3fh)mQbuKMLR!ik7tf21fK>nPY?0mJA?iLQG}uBu8(2ea zEXHd`SSj1b=r}V<{(B5E_ZGmV!)j3?*zr`&LD1M?S2$v5F)tB|LgAmrh$L8BQ?Hd;)5yir)B+Wx_Qc@u-cn5_xy#9;DqYlL zTl@sZ(9|{7i(|Nj_(h!4rMHrvV+c&A%3rfOPV)lq42{>rBIaT%?>mx;Cw(BjcJn57 zxN^!q(WQGD=gH^ZwGvpLI2~T676FH$R(6tV?BxuJqAZFiCMB7Ym1`DNMJe5E&}5Ia zi)tmeQH41HF9*9;y(xDny_Z&ZM(Me?bnVbWc9YpSSNT?VcPhEDPAJ2fvdu$WyDs`9 zMd7CJPabz05tJmUZUQhGEk@E_BC+|b>0Cw~rd{@%sjIPWnl`dl@g!Nwt0Vn%?u5{Y z*Dso!Y$$OYov$)*V1RTZF6eN4|Oe7 zc2s7H6MT2!iX2*B>P;zMc7t(7158?_M_T&&fXvl34>+-3x*dd7e@aqIqz9j+v>miS zmwT1+G}0F(^R{9;g7Dt!!4_L5ryBnfk4vhau2Mp)QD?K9&sdSI5(9XjGl((Lk%gdM zmu!4W3&Iq$>m;k9NT_^e_(Vgg3v)XDm!c?i+!wPlP@*YKrXM$X*%vf7Au7f|4wkaC zg5Ji^(&Ob&&LckhRF# zRyF~cxyu92Pz?3*((uJ-EV*b4n?ZE^&$b%Fz z!ME>`xU{_rCql=g5w+X%6uAe93O-aSgm2%wbe^PoJdN+Ho@XYT6x*?t%_*yGH<6VZ z`q@Dtgn47H=d6&+TZB_0&dJ`jL--vm0bn-LO~B|o8Mfm{uU`p<&9(@WW){a@>OQf z?Vd0h&a7$2XbD+kjit!TR!{KLjP~~-kzb(+K6lGL0Y0rBU`jcN`u~07DP78HNGNX4 z#D~m!Gnx<4BE=(3E#%*XzGxcRm3f^SB+^4F{L+XvA&hK}A&9wfF(m$y&z*JDos)$Jm|40(kM1`WJpIhkOAf{z=(9v2hv`m&>2OSx?qQFE=OEp2K4M9PU8r~MWhqB2+ z7V+{#m9N%Hv3M9D8ngMB7PA!FjPjHw+q%EoqRZsxB!#z$qo@*Ngr02L z`0ws+Dec>G?zz4%Z49?k%~c_pF)pfVOT~9O#|kS2m9Zw9xt+%5%I8AIg(`>gy8E=Y z(C@j#SD3lzLB$3mv(ub=&Q!c1tuL+N+cs!v59WmZG(MM8(4DQN!td;d=q$`L$_^Xhn!{F zI7ibVUs@Y&V;!a_Szqb1y0*;j?vVTU z_auHlCG-C<_578d;hThkIQ9@FT8${4veAk^@=g zvo@Qtd`mxwErqp7(WO>KGF~r6bYEqy!3J5}%d%3VSsRa6Q>Yf7!~nJKD!h(Jt#*iB zXVir&gz&8215wUs2cTmg6HNy71x1I4M#u+dv}d5IQKLx=D&Hzejf87CMgAC@+~wr3wj?fa*0d9j6qOrWn?u6Y zwSdH=0&;s3B5G2bNZLDo_ zO^X+#kajCQEN7&mkMad*Noo4|Yh-)6gb!S=Y`P z<&cY6z#)gYEb45RQ>D&McvwSj6PG@j619^MSGy{*OgoFrAyaDVk^PD3w60mxldJih zVCJ08K&{L~jg{pvty06vc3`dX6^U;kV|LXtw zDOWDvB1WfOn0_IRWU^aZaQgIN{)hkZvz$0GE7*mOoH6)_uOr|6>v#F*|Kb6@o=`Wn zmK|+mr?Kh}Q`o0bHJ7vPY&^$%?%0ma!g8d3C4kR69CskLL)12a|7zG?CfMs7I?q6)F|v!jdiV{FRc&@|HY zx}!;%=D&cyH7+Z=MC}&gAy84s=Y;7b2}J@Xn1>QayZuT^5lpWY+n!@-&lXj=7V?N( z#TjXGlEzf3*h*fL5L(xL)Qdf3uNT$`wkSjGZ0>-_O7zfS|7rG9M{N~#|7KP36$yIo z$uHkDlC{z+O5MFlH&_%Bmr%q`gk!4KlSjo`N7FR_KUweotZ8=D_kGve`+3hfeeap+ zo}Q~T8g!Qc2_!Cx3JYV16UrfWRpP{^Dx}J}ROOH0Pa&1E%P|y=Qw9Q-0U>lnNJwG? zGh#H7Msw?)zIUH<-uKyit^BYqd%uHSlnc*H_c`zTJbSOp_xt&JMzB|C950!J#AJjO zDH>{7;GO759`p-oJV2CL9MS2Z>G(7K1uzcR)DpU!xBJqZvig} zVty4M>RuIfN7J*9U937RrC5>5gnDpb6_`_i$*a4%n9Aew#j|+d2cO2JN6zErmwtf1 zed$N&r>4~s@4SfAQW&YBMu+FnOKYQ@Q3l4ra)8U1AHnm_U&dpPR5V?1?))JR4mQ|& z&=;CusT&&;@`6$d&YeGpC$F8yrAx?UK@p&d9rD|gjvxK#E~M?i)Ir)Dn&+*$s)d#L z@2XL)g7Wu|9awj{I=Nr0weF3Wb!fxAZ7sjWleS#ght0{xPLW{|n4LHbV@BuLafx~O zkZ%qj#NRKD;sBfd1TiWMaSS@J`17RbxnE4XsH?$>ANYM5_f5GR2)Qj!BSxg!3zLa(R*BADmhhy&M}E?}nOoJ8Q@if} zK0^$G6bKPu5CrF6|FbJd8ko&6_jol zla6xA^K5BTo$t~Tm##YLxM8zw%Dmr_&_;;ilylbH{lO96&D$n*%2oFpl^yt1*tOv@ z;gqMKm*Dkhgea0r6PdrE?;rZ>NJq4BSNKpWRdjC|L6Sj7L$2p-r}`qxouHhUg3!%- z*TR}P={Bw_K3iO{Y>rTB!Ll6S?BN+4E%t?%!q^;aJZNf#>aArJnU9iMZD^zu4y;5C9Lgdkr z!6gZn%YEtGS)4gILg5Wg*DX+YAn^;GD@xrMyT|>VcW-xm^{cPq_RR-4KG~tShMPBT z;~U?2(O68boo5n@9`J5sR=FTDCVkT?I(5ems{;UP?$d}uLg8v7>;5!UNIo(uT%fFp z;TjP!QQOtN;A!Xd2B3 zQyZq+en+Q5E9dh%s&Zf&gHMsD_PI5ehuXLbthpl{5?Q}sJcq}oCqACRsO)EyNGLXA zazbt=l`xU!;XaThnnt})6Hlll4ndEP_Pk&SMP}TNn&Z7OQZoNXO}TUTsERzdblL+) zY6{UFI98p}SIE?hwl6Mu$y4lmX`Ew04@Zz7n9LB^6_tw9CnU+|pz48ueF_x}pN+k< z#tR(gMZ1*PI*`c9NLr)gEtEP+saPsQgs^L;sGj{1roxzi7H8(KtzlW}UJE#!*<(Hz zv(wn{S6}qlB{MiLnn5Wh>l3uS{PU43G9J1f44ITIQc7`|N9U1dDH=;tR^C&xgiF_E zMw)2my#C_inq1K0GRDd^l`SD}mMI6jmII({aI_p@IXJ-i3m0+u@&#?DB@YGXJ;=+Y9SPl-L zs#t1Ca_DwF!P{@&#LZjx(N>pTvf^;F;PS;wxOm|VngZ)?i+lGU8ijYaf>2PGip!TS z;K`>i<6X}`iYFeoJoIPZbqOE-@%P}`+gGvOZt?1m-o(v2r;uGoDF--z{w$ulat>$D z9O3TW`#4z}&YwGnb7wXHE3RL^i+lG^v35mkUR|%|skrj^Wn8#;9w(=#xOw9y?%a6* z*%{}@B7M7M!YkwL>&N)QpTB}PuHVAL<72F+r#L=+i0#QKw%x|I3U~I$_)0b5k`nTq zbZGmt9DXMq&!{abyYCXai6CMO8ZWk+bcxv#^?1zptE0yg^rzjrR-H_0F3lEq#>ccM z$;~d+73W%2lpOhpG!$lE#4DN%3AF7O{0+#ifreyT2aj${AY7f{OL4}8{FvS+5#&Un z)@s#%WRdsPMy@GqCLiI=@2?vI=P)?Gyg{Be8D#80yhqoi5kqZ7Z-81S4yy|fXg`uW1SJ?uEP|$}A(1W9a^22q z897US-II^a;LjXI(N64pPjSO-wK1?e_C?0-+E8EOYXV2P7ohc>?Mp*yyOfP9sr<*# z;RA^=6T3M|GXj#WBJf=Sxs1(p=k`l!n4mD3eHfFvh211Kd0g?3zMZ%vVyUq%nJ8Qjz7FdGetv#R6PLw`O_FWvD?| z%pFdIf&wlZlzIjiFI~ouf9$>ZwO{)P9)IF26pGW+4mm1*xfYx`I*0S;FXKZWeINeY zUwsZICp)};;~3As=Q55Ct=0U&_iy9(e*drV?H9g>;}gZG;$Q!pui;0p-^S6=8JszHfS>>L^Z4|qp1~uR&R}f~ZS8pRJ8$C; z{_xv)_4S(&S@7(;pTa-;rys_T{n$A?^5`J~3HR)?XYm{V^G~3ymSgf4Uw#e$*T4P> z?%qAc;hA&z)&J&W_@z%@!2GZ%4qP|>^MFTZvjzxzAi!ogCqs5yI{ z@hkt;kKxfr4{&s_VC@~ZZk*uX|IWAZtruR!!-ox}9^l-$GdS2-m$j}9r#r#A1xO2) zxRFHG6Y?Y8UnCVuA}j)i>I+}?%9i|{u*6Gss3Y$wdAfGP zzhsQCq(Lv@mdOv&I>nTVFRr`9B6kG62AQGz*ukpC5LTFBao3Q*(s{(hX%fYlxr>eI zK!Zqa2UY_`{<&YY%t#SHKui_>&xpXq(B*T%7x^5svNDX8k;$#@AyFEZl{naClHJ2` zBkD_EayV;%0n<7_Q|f4>7bXThTab55HpX~qoGU%YP2