From 3c3a185200fb92281128d0d84e0697da21f7dc09 Mon Sep 17 00:00:00 2001 From: asi345 Date: Wed, 22 May 2024 11:17:24 +0200 Subject: [PATCH] bitbox02: add bitbox02 simulator and tests Signed-off-by: asi345 --- .github/actions/install-sim/action.yml | 7 ++ .github/workflows/ci.yml | 3 + ci/build_bitbox02.sh | 9 ++ ci/cirrus.Dockerfile | 5 ++ hwilib/devices/bitbox02.py | 120 ++++++++++++++++--------- test/README.md | 45 ++++++++-- test/run_tests.py | 14 ++- test/setup_environment.sh | 35 ++++++++ test/test_bitbox02.py | 109 ++++++++++++++++++++++ test/test_coldcard.py | 1 + test/test_device.py | 32 ++++--- test/test_digitalbitbox.py | 1 + test/test_jade.py | 1 + test/test_keepkey.py | 1 + test/test_ledger.py | 1 + test/test_trezor.py | 1 + 16 files changed, 325 insertions(+), 60 deletions(-) create mode 100755 ci/build_bitbox02.sh create mode 100644 test/test_bitbox02.py diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index f9799f122..abfbded13 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -38,6 +38,13 @@ runs: apt-get install -y libusb-1.0-0 tar -xvf mcu.tar.gz + - if: inputs.device == 'bitbox02' + shell: bash + run: | + apt-get update + apt-get install -y libusb-1.0-0 docker.io + tar -xvf bitbox02.tar.gz + - if: inputs.device == 'jade' shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96b2ed369..bbf858dea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,7 @@ jobs: - { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' } - { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' } - { name: 'keepkey', archive: 'keepkey-firmware', paths: 'test/work/keepkey-firmware/bin' } + - { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' } steps: - uses: actions/checkout@v4 @@ -219,6 +220,7 @@ jobs: - 'ledger' - 'ledger-legacy' - 'keepkey' + - 'bitbox02' script: - name: 'Wheel' install: 'pip install dist/*.whl' @@ -289,6 +291,7 @@ jobs: - 'ledger' - 'ledger-legacy' - 'keepkey' + - 'bitbox02' interface: [ 'library', 'cli', 'stdin' ] container: python:${{ matrix.python-version }} diff --git a/ci/build_bitbox02.sh b/ci/build_bitbox02.sh new file mode 100755 index 000000000..29b8a0bb2 --- /dev/null +++ b/ci/build_bitbox02.sh @@ -0,0 +1,9 @@ +docker volume rm bitbox02_volume || true +docker volume create bitbox02_volume +CONTAINER_VERSION=$(curl https://raw.githubusercontent.com/BitBoxSwiss/bitbox02-firmware/master/.containerversion) +docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION +docker run -i --rm -v bitbox02_volume:/bitbox02-firmware shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c \ + "cd /bitbox02-firmware && \ + git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git . && \ + git config --global --add safe.directory ./ && \ + make -j simulator" \ No newline at end of file diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile index ab4c11b10..17d6752d8 100644 --- a/ci/cirrus.Dockerfile +++ b/ci/cirrus.Dockerfile @@ -59,6 +59,11 @@ RUN protoc --version # docker build -f ci/cirrus.Dockerfile -t hwi_test . # docker run -it --entrypoint /bin/bash hwi_test # cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only +# For BitBox02: +# docker build -f ci/cirrus.Dockerfile -t hwi_test . +# ./ci/build_bitbox02.sh +# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test +# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only #################### #################### diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 173f9997d..9484e0aae 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -19,6 +19,7 @@ import base64 import builtins import sys +import socket from functools import wraps from .._base58 import decode_check, encode_check @@ -79,6 +80,8 @@ BitBoxNoiseConfig, ) +SIMULATOR_PATH = "127.0.0.1:15423" + class BitBox02Error(UnavailableActionError): def __init__(self, msg: str): """ @@ -178,10 +181,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain 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()) + devs = [device_info["path"].decode() for device_info in devices.get_any_bitbox02s()] + if allow_emulators: + devs.append(SIMULATOR_PATH) + for path in devs: + client = Bitbox02Client(path=path) + if allow_emulators and client.simulator and not client.simulator.connected: + continue + if path != SIMULATOR_PATH: + client.set_noise_config(SilentNoiseConfig()) d_data: Dict[str, object] = {} bb02 = None with handle_errors(common_err_msgs["enumerate"], d_data): @@ -252,9 +260,31 @@ def func(*args, **kwargs): # type: ignore raise exc except FirmwareVersionOutdatedException as exc: raise DeviceNotReadyError(str(exc)) + except ValueError as e: + raise BadArgumentError(str(e)) return cast(T, func) +class BitBox02Simulator(): + def __init__(self) -> None: + self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ip, port = SIMULATOR_PATH.split(":") + self.connected = True + try: + self.client_socket.connect((ip, int(port))) + except: + self.connected = False + + def write(self, data: bytes) -> None: + # Messages from client are always prefixed with HID report ID(0x00), which is not expected by the simulator. + self.client_socket.send(data[1:]) + + def read(self, size: int, timeout_ms: int) -> bytes: + res = self.client_socket.recv(64) + return res + + def close(self) -> None: + self.client_socket.close() # This class extends the HardwareWalletClient for BitBox02 specific things class Bitbox02Client(HardwareWalletClient): @@ -267,17 +297,23 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal "The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock." ) super().__init__(path, password=password, expert=expert, chain=chain) - - hid_device = hid.device() - hid_device.open_path(path.encode()) - self.transport = u2fhid.U2FHid(hid_device) + self.simulator = None + self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig() + + if path != SIMULATOR_PATH: + hid_device = hid.device() + hid_device.open_path(path.encode()) + self.transport = u2fhid.U2FHid(hid_device) + self.noise_config = CLINoiseConfig() + else: + self.simulator = BitBox02Simulator() + if self.simulator.connected: + self.transport = u2fhid.U2FHid(self.simulator) 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 @@ -285,38 +321,32 @@ def init(self, expect_initialized: Optional[bool] = True) -> 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: - continue - - 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 - is_initialized = bb02.device_info()["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." + bb02 = bitbox02.BitBox02( + transport=self.transport, + # Passing None as device_info means the device will be queried for the relevant device info. + device_info=None, + 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 + is_initialized = bb02.device_info()["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." + ) - return bb02 - raise Exception( - "Could not find the hid device info for path {}".format(self.device_path) - ) + return bb02 def close(self) -> None: self.transport.close() @@ -883,9 +913,13 @@ def setup_device( if label: bb02.set_device_name(label) - if not bb02.set_password(): - return False - return bb02.create_backup() + if self.device_path != SIMULATOR_PATH: + if not bb02.set_password(): + return False + return bb02.create_backup() + else: + bb02.restore_from_mnemonic() + return True @bitbox02_exception def wipe_device(self) -> bool: diff --git a/test/README.md b/test/README.md index c064d8786..c3efb9262 100644 --- a/test/README.md +++ b/test/README.md @@ -20,16 +20,19 @@ It also tests usage with `bitcoind`. - `test_jade.py` tests the command line interface and Blockstream Jade implementation. It uses the [Espressif fork of the Qemu emulator](https://github.com/espressif/qemu.git). It also tests usage with `bitcoind`. +- `test_bitbox02.py` tests the command line interface and the BitBox02 implementation. +It uses the [BitBox02 simulator](https://github.com/BitBoxSwiss/bitbox02-firmware/tree/master/test/simulator). +It also tests usage with `bitcoind`. -`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, and `bitcoind`. -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. +`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, the BitBox02 simulator and `bitcoind`. +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`, `work/test/bitbox02-firmware` 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`, `--jade`, and `--bitcoind`. +These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, `--bitbox02` 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, Jade emulator, and bitcoind. +`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, Jade emulator, BitBox02 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`, `test_jade.py`, and `test/test_digitalbitbox.py` can be disabled. +`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, `test_bitbox02.py` and `test/test_digitalbitbox.py` can be disabled. If you are building the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Jade emulator, the Digital Bitbox simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`. @@ -329,6 +332,38 @@ You also have to install its python dependencies pip install -r requirements.txt ``` +## BitBox02 Simulator + +### Dependencies + +In order to build the BitBox02 simulator, the following packages will need to be installed: + +``` +apt install docker.io +``` + +### Building + +Clone the repository: + +``` +git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git +``` + +Pull the BitBox02 firmware Docker image: + +``` +cd bitbox02-firmware +make dockerpull +``` + +Build the simulator: + +``` +make dockerdev +make simulator +``` + ## Bitcoin Core diff --git a/test/run_tests.py b/test/run_tests.py index 9bfcd364e..cd502a6c0 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -16,6 +16,7 @@ from test_digitalbitbox import digitalbitbox_test_suite from test_keepkey import keepkey_test_suite from test_jade import jade_test_suite +from test_bitbox02 import bitbox02_test_suite from test_udevrules import TestUdevRulesInstaller parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests') @@ -51,6 +52,10 @@ 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') +dbb_group = parser.add_mutually_exclusive_group() +dbb_group.add_argument('--no_bitbox02', dest='bitbox02', help='Do not run BitBox02 test with simulator', action='store_false') +dbb_group.add_argument('--bitbox02', dest='bitbox02', help='Run BitBox02 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 Coldcard simulator', default='work/firmware/unix/simulator.py') @@ -58,6 +63,7 @@ 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('--jade-path', dest='jade_path', help='Path to Jade qemu emulator', default='work/jade/simulator') +parser.add_argument('--bitbox02-path', dest='bitbox02_path', help='Path to BitBox02 simulator', default='work/bitbox02-firmware/build-build/bin/simulator') 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/build/src/bitcoind') @@ -65,7 +71,7 @@ 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, ledger_legacy=None, jade=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None) args = parser.parse_args() @@ -92,6 +98,7 @@ args.ledger = True if args.ledger is None else args.ledger args.ledger_legacy = True if args.ledger_legacy is None else args.ledger_legacy args.jade = True if args.jade is None else args.jade + args.bitbox02 = True if args.bitbox02 is None else args.bitbox02 else: # Default all false unless overridden args.trezor_1 = False if args.trezor_1 is None else args.trezor_1 @@ -102,8 +109,9 @@ args.ledger = False if args.ledger is None else args.ledger args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy args.jade = False if args.jade is None else args.jade + args.bitbox02 = False if args.bitbox02 is None else args.bitbox02 -if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade: +if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02: # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) @@ -123,5 +131,7 @@ success &= ledger_test_suite(args.ledger_path, bitcoind, args.interface, True) if success and args.jade: success &= jade_test_suite(args.jade_path, bitcoind, args.interface) + if success and args.bitbox02: + success &= bitbox02_test_suite(args.bitbox02_path, bitcoind, args.interface) sys.exit(not success) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index bbb69b68d..d4f1463d7 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -34,6 +34,10 @@ while [[ $# -gt 0 ]]; do build_jade=1 shift ;; + --bitbox02) + build_bitbox02=1 + shift + ;; --bitcoind) build_bitcoind=1 shift @@ -46,6 +50,7 @@ while [[ $# -gt 0 ]]; do build_ledger=1 build_keepkey=1 build_jade=1 + build_bitbox02=1 build_bitcoind=1 shift ;; @@ -416,6 +421,36 @@ if [[ -n ${build_jade} ]]; then cd .. fi +if [[ -n ${build_bitbox02} ]]; then + # Clone digital bitbox02 firmware if it doesn't exist, or update it if it does + if [ ! -d "bitbox02-firmware" ]; then + git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git + cd bitbox02-firmware + else + cd bitbox02-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 + fi + fi + + # Build the simulator. This is cached, but it is also fast + CONTAINER_VERSION=$(cat .containerversion) + docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION + # The safe.directory config is so that git commands work. even though the repo folder mounted in + # Docker is owned by root, which can be different from the owner on the host. + docker run -i --rm --volume $(pwd):/bb02 shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c "git config --global --add safe.directory /bb02 && cd /bb02 && make -j simulator" +fi + if [[ -n ${build_bitcoind} ]]; then # Clone bitcoind if it doesn't exist, or update it if it does bitcoind_setup_needed=false diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py new file mode 100644 index 000000000..fb9cf8490 --- /dev/null +++ b/test/test_bitbox02.py @@ -0,0 +1,109 @@ +#! /usr/bin/env python3 + +import atexit +import os +import subprocess +import time +import unittest +import sys +import argparse + +from hwilib.devices.bitbox02 import Bitbox02Client + +from test_device import ( + DeviceEmulator, + DeviceTestCase, + TestDeviceConnect, + TestDisplayAddress, + TestGetKeypool, + TestGetDescriptors, + TestSignTx, +) + +# Class for emulator control +class BitBox02Emulator(DeviceEmulator): + def __init__(self, simulator): + self.simulator = simulator + self.path = "127.0.0.1:15423" + self.type = "bitbox02" + self.fingerprint = "4c00739d" + self.master_xpub = "tpubDDoFYQF4zAhrW8LRtCxePR8bJsAh5SXU6PwPNi2oRfeh67qhmxZawJ4m3V76P8AYSEueKmwvNyiSPAGYtynGfzJNvTHyzj2FJTbp729jmYM" + self.password = None + self.supports_ms_display = False + self.supports_xpub_ms_display = False + self.supports_unsorted_ms = False + self.supports_taproot = False + self.strict_bip48 = False + self.include_xpubs = True + self.supports_device_multiple_multisig = True + self.supports_legacy = False + + def start(self): + super().start() + self.log = open('bitbox02-simulator.stderr', 'a') + # Start the Bitbox02 simulator + self.simulator_proc = subprocess.Popen( + [ + './' + os.path.basename(self.simulator) + ], + cwd=os.path.dirname(self.simulator), + stderr=self.log + ) + time.sleep(1) + + self.setup_client = Bitbox02Client(self.path) + self.setup_bb02 = self.setup_client.restore_device() + self.setup_client.close() + + atexit.register(self.stop) + + def stop(self): + super().stop() + self.simulator_proc.terminate() + self.simulator_proc.wait() + self.log.close() + atexit.unregister(self.stop) + +class TestBitbox02GetXpub(DeviceTestCase): + def test_getxpub(self): + self.dev_args.remove('--chain') + self.dev_args.remove('test') + result = self.do_command(self.dev_args + ['--expert', 'getxpub', 'm/84h/0h/0h/3']) + self.assertEqual(result['xpub'], 'xpub6F8W4c3nJf6vWyEQPW9rofRgKf9LUWrbLc6fh2GUgofxXzuMwNEXw9dUuAeHuNiu2MebTmLX1CY2wxN1pgUuQtsWa9x8QBk7J51nD86vann') + self.assertFalse(result['testnet']) + self.assertFalse(result['private']) + self.assertEqual(result['depth'], 4) + self.assertEqual(result['parent_fingerprint'], 'd934efde') + self.assertEqual(result['child_num'], 3) + self.assertEqual(result['chaincode'], '03b0d37df586659fb87145e1d28506e4e2d42777586568d61ecdf6c9e041a0a1') + self.assertEqual(result['pubkey'], '03290b94a942a317c3846244f1eb6d67214326c8cfc6d940c823ace57ab818dbbd') + +def bitbox02_test_suite(simulator, bitcoind, interface): + dev_emulator = BitBox02Emulator(simulator) + + signtx_cases = [ + (["segwit"], [], False, False) + ] + + # Generic Device tests + suite = unittest.TestSuite() + suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, bitcoind, emulator=dev_emulator, interface=interface, detect_type=dev_emulator.type)) + suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestGetDescriptors, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases)) + suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface)) + # TestSignMessage is removed, since its only testcase is for legacy p2pkh, which is not supported by BitBox02 + # suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface)) + suite.addTest(DeviceTestCase.parameterize(TestBitbox02GetXpub, bitcoind, emulator=dev_emulator, interface=interface)) + + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) + return result.wasSuccessful() + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Test BitBox02 implementation') + parser.add_argument('simulator', help='Path to simulator binary') + 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') + args = parser.parse_args() + + sys.exit(not bitbox02_test_suite(args.simulator, args.bitcoind, args.interface)) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 56ac8ea5a..9eb90f379 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -44,6 +44,7 @@ def __init__(self, simulator): self.strict_bip48 = False self.include_xpubs = False self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start() diff --git a/test/test_device.py b/test/test_device.py index eb7fe501c..887a6ea65 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -35,6 +35,7 @@ def __init__(self): self.strict_bip48 = None self.include_xpubs = None self.supports_device_multiple_multisig = None + self.supports_legacy = None def start(self): assert self.type is not None @@ -49,6 +50,7 @@ def start(self): assert self.strict_bip48 is not None assert self.include_xpubs is not None assert self.supports_device_multiple_multisig is not None + assert self.supports_legacy is not None def stop(self): pass @@ -584,21 +586,21 @@ def test_signtx(self): # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): # make a huge transaction - keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "legacy", "0", "100"]) + keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "sh_wit", "0", "100"]) import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) outputs = [] num_inputs = 60 for i in range(0, num_inputs): - outputs.append({self.wrpc.getnewaddress('', 'legacy'): 0.001}) - outputs.append({self.wrpc.getnewaddress("", "legacy"): 10}) + outputs.append({self.wrpc.getnewaddress('', "p2sh-segwit"): 0.001}) + outputs.append({self.wrpc.getnewaddress("", "p2sh-segwit"): 10}) psbt = self.wpk_rpc.walletcreatefundedpsbt([], outputs, 0, {}, True)['psbt'] psbt = self.wpk_rpc.walletprocesspsbt(psbt)['psbt'] tx = self.wpk_rpc.finalizepsbt(psbt)['hex'] self.wpk_rpc.sendrawtransaction(tx) self.wpk_rpc.generatetoaddress(10, self.wpk_rpc.getnewaddress()) inputs = self.wrpc.listunspent() - psbt = self.wrpc.walletcreatefundedpsbt(inputs, [{self.wpk_rpc.getnewaddress('', 'legacy'): 0.001 * num_inputs}])['psbt'] + psbt = self.wrpc.walletcreatefundedpsbt(inputs, [{self.wpk_rpc.getnewaddress('', "p2sh-segwit"): 0.001 * num_inputs}])['psbt'] # For cli, this should throw an exception try: result = self.do_command(self.dev_args + ['signtx', psbt]) @@ -614,9 +616,14 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def test_display_address_path(self): result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "legacy", '--path', 'm/44h/1h/0h/0/0']) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) + if self.emulator.supports_legacy: + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + else: + self.assertIn('error', result) + self.assertIn('code', result) + self.assertEqual(result['code'], -9) result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "sh_wit", '--path', 'm/49h/1h/0h/0/0']) self.assertNotIn('error', result) @@ -657,9 +664,14 @@ def test_display_address_descriptor(self): # Legacy address result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'pkh([' + self.emulator.fingerprint + '/44h/1h/0h]' + legacy_account_xpub + '/0/0)']) - self.assertNotIn('error', result) - self.assertNotIn('code', result) - self.assertIn('address', result) + if self.emulator.supports_legacy: + self.assertNotIn('error', result) + self.assertNotIn('code', result) + self.assertIn('address', result) + else: + self.assertIn('error', result) + self.assertIn('code', result) + self.assertEqual(result['code'], -9) # Should check xpub result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.emulator.fingerprint + '/84h/1h/0h]' + "not_and_xpub" + '/0/0)']) diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 297e01181..d43a9af65 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -42,6 +42,7 @@ def __init__(self, simulator): self.strict_bip48 = False self.include_xpubs = False self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start() diff --git a/test/test_jade.py b/test/test_jade.py index 1d57c4fc4..710bf914f 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -52,6 +52,7 @@ def __init__(self, jade_qemu_emulator_path): self.strict_bip48 = False self.include_xpubs = False self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start() diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 45ccba21f..f926d9807 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -64,6 +64,7 @@ def __init__(self, path): self.strict_bip48 = False self.include_xpubs = False self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start() diff --git a/test/test_ledger.py b/test/test_ledger.py index a9a5030af..26876c0df 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -46,6 +46,7 @@ def __init__(self, path, legacy=False): self.strict_bip48 = True self.include_xpubs = True self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start() diff --git a/test/test_trezor.py b/test/test_trezor.py index 0b70e0ee4..f7424ce90 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -63,6 +63,7 @@ def __init__(self, path, model): self.strict_bip48 = True self.include_xpubs = False self.supports_device_multiple_multisig = True + self.supports_legacy = True def start(self): super().start()