diff --git a/.github/actions/build-bitcoind/action.yml b/.github/actions/build-bitcoind/action.yml index ed4c5957c..e4befd42e 100644 --- a/.github/actions/build-bitcoind/action.yml +++ b/.github/actions/build-bitcoind/action.yml @@ -23,7 +23,7 @@ runs: ccache --zero-stats cd test; ./setup_environment.sh --bitcoind; cd .. ccache --show-stats --verbose - tar -czf bitcoind.tar.gz test/work/bitcoin/build/src/bitcoind + tar -czf bitcoind.tar.gz test/work/bitcoin/build/bin/bitcoind - uses: actions/cache/save@v4 if: github.event_name != 'pull_request' && steps.ccache-cache.outputs.cache-hit != 'true' diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml new file mode 100644 index 000000000..323eb5f7e --- /dev/null +++ b/.github/actions/build-sim/action.yml @@ -0,0 +1,28 @@ +name: Build sim +description: Build device simulator(s) +runs: + using: composite + steps: + - name: Install dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y gcc-arm-linux-gnueabihf libsdl2-image-dev libslirp-dev libpcsclite-dev ninja-build + pip install poetry + wget https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip + sudo unzip protoc-22.0-linux-x86_64.zip -d /usr/local + protoc --version + + - name: Build simulator + shell: bash + run: | + git config --global user.email "ci@ci.com" + git config --global user.name "ci" + cd test; ./setup_environment.sh --${{ matrix.device.name }}; cd .. + tar -czf ${{ matrix.device.archive }}.tar.gz ${{ matrix.device.paths }} + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.device.name }}-sim + path: ${{ matrix.device.archive }}.tar.gz + diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index abfbded13..3cc846811 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -30,6 +30,8 @@ runs: pushd test/work/firmware; git am ../../data/coldcard-multisig.patch; popd poetry run pip install -r test/work/firmware/requirements.txt pip install -r test/work/firmware/requirements.txt + poetry run pip install pysdl2-dll + pip install pysdl2-dll - if: inputs.device == 'bitbox01' shell: bash @@ -58,13 +60,14 @@ runs: apt-get update apt-get install -y libusb-1.0-0 qemu-user-static tar -xvf speculos.tar.gz - poetry run pip install construct flask-restful jsonschema mnemonic pyelftools pillow requests pytesseract - pip install construct flask-restful jsonschema mnemonic pyelftools pillow requests pytesseract + poetry run pip install construct flask-cors flask-restful jsonschema ledgered mnemonic pyelftools pillow requests pytesseract + pip install construct flask-cors flask-restful jsonschema ledgered mnemonic pyelftools pillow requests pytesseract - if: inputs.device == 'ledger' uses: actions/download-artifact@v4 with: - name: ledger_app + name: ${{ inputs.device == 'ledger-legacy' && 'ledger_app_nano_s' || 'ledger_app_nano_x' }} + - if: inputs.device == 'ledger' shell: bash diff --git a/.github/actions/test-device/action.yml b/.github/actions/test-device/action.yml new file mode 100644 index 000000000..4bf20a7b1 --- /dev/null +++ b/.github/actions/test-device/action.yml @@ -0,0 +1,31 @@ +name: Test device +description: Run tests for one device type. +runs: + using: composite + steps: + - shell: bash + run: | + pip install poetry + poetry install + + - uses: actions/download-artifact@v4 + with: + name: bitcoind + + - shell: bash + run: | + tar -xvf bitcoind.tar.gz + + - uses: ./.github/actions/install-sim + with: + device: ${{ matrix.device }} + + - name: Run tests + shell: bash + run: | + cd test; poetry run ./run_tests.py --${{ matrix.device }} --interface=${{ matrix.interface }} --device-only; cd .. + + - if: failure() + shell: bash + run: | + tail -v -n +1 test/*.std* diff --git a/.github/actions/test-dist/action.yml b/.github/actions/test-dist/action.yml new file mode 100644 index 000000000..99fab7e96 --- /dev/null +++ b/.github/actions/test-dist/action.yml @@ -0,0 +1,67 @@ +name: Test dist +description: Run dist tests for one device type. +runs: + using: composite + steps: + - shell: bash + run: | + pip install poetry + + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Install (Wheel) + if: matrix.script == 'Wheel' + shell: bash + run: | + pip install dist/*.whl + + - name: Install (Sdist) + if: matrix.script == 'Sdist' + shell: bash + run: | + pip install $(find dist -name "*.tar.gz" -a -not -name "*linux*") + + - name: Install (Bindist) + if: matrix.script == 'Bindist' + shell: bash + run: | + poetry install; cd dist; tar -xvf hwi*linux*.tar.gz; cd .. + + - uses: actions/download-artifact@v4 + with: + name: bitcoind + + - shell: bash + run: | + tar -xvf bitcoind.tar.gz + + - uses: ./.github/actions/install-sim + with: + device: ${{ matrix.device }} + + - name: Run tests (Wheel) + if: matrix.script == 'Wheel' + shell: bash + run: | + cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + + - name: Run tests (Sdist) + if: matrix.script == 'Sdist' + shell: bash + run: | + cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + + - name: Run tests (Bindist) + if: matrix.script == 'Bindist' + shell: bash + run: | + cd test; poetry run ./run_tests.py $DEVICE --interface=bindist --device-only; cd .. + + - if: failure() + shell: bash + run: | + tail -v -n +1 test/*.std* + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbf858dea..b44af1633 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,13 @@ name: CI on: + # See: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request. pull_request: + # See: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push. push: - branches: ['**'] - tags-ignore: ['**'] + branches: + - '**' + tags-ignore: + - '**' concurrency: group: ${{ github.event_name != 'pull_request' && github.run_id || github.ref }} @@ -91,7 +95,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: docker/build-push-action@v5 + - uses: docker/build-push-action@v6 with: context: . file: contrib/build-wine.Dockerfile @@ -115,7 +119,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: docker/build-push-action@v5 + - uses: docker/build-push-action@v6 with: context: . file: contrib/build.Dockerfile @@ -138,9 +142,12 @@ jobs: name: dist path: dist/ - sim-builder: - name: Sim builder - runs-on: ubuntu-latest + sim-builder-trezor: + name: Trezor sim builder + # Ubuntu 22.04 ships with glibc 2.35, which is needed to keep Trezor 1 + # binaries compatible with Debian Bookworm (glibc 2.36) Python containers. + # Trezor T binaries don't need this. + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -148,89 +155,199 @@ jobs: device: - { name: 'trezor-1', archive: 'trezor-firmware', paths: 'test/work/trezor-firmware' } - { name: 'trezor-t', archive: 'trezor-firmware', paths: 'test/work/trezor-firmware' } + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-sim + + sim-builder-coldcard: + name: Coldcard sim builder + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + device: - { name: 'coldcard', archive: 'coldcard-mpy', paths: 'test/work/firmware/external/micropython/ports/unix/coldcard-mpy test/work/firmware/unix/coldcard-mpy test/work/firmware/unix/l-mpy test/work/firmware/unix/l-port' } + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-sim + + sim-builder-bitbox: + name: Bitbox sim builder + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + device: - { name: 'bitbox01', archive: 'mcu', paths: 'test/work/mcu' } + - { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' } + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-sim + + sim-builder-jade: + name: Jade sim builder + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + device: - { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' } + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-sim + + sim-builder-ledger: + name: Ledger sim builder + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + device: - { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' } + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-sim + + sim-builder-keepkey: + name: Keepkey sim builder + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + device: - { 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 + - uses: ./.github/actions/build-sim - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y gcc-arm-linux-gnueabihf libsdl2-image-dev libslirp-dev libpcsclite-dev ninja-build - pip install poetry - wget https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip - sudo unzip protoc-22.0-linux-x86_64.zip -d /usr/local - protoc --version - - name: Build simulator - run: | - git config --global user.email "ci@ci.com" - git config --global user.name "ci" - cd test; ./setup_environment.sh --${{ matrix.device.name }}; cd .. - tar -czf ${{ matrix.device.archive }}.tar.gz ${{ matrix.device.paths }} + ledger-s-app-builder: + name: Ledger Nano S Bitcoin App builder + runs-on: ubuntu-latest + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + steps: + - run: | + git clone https://github.com/LedgerHQ/app-bitcoin-new.git + cd app-bitcoin-new + make DEBUG=1 - uses: actions/upload-artifact@v4 with: - name: ${{ matrix.device.name }}-sim - path: ${{ matrix.device.archive }}.tar.gz + name: ledger_app_nano_s + path: app-bitcoin-new/bin/app.elf + - ledger-app-builder: - name: Ledger App builder + ledger-x-app-builder: + name: Ledger Nano X Bitcoin App builder runs-on: ubuntu-latest container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest steps: - run: | git clone https://github.com/LedgerHQ/app-bitcoin-new.git cd app-bitcoin-new - make DEBUG=1 + make DEBUG=1 BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 with: - name: ledger_app + name: ledger_app_nano_x path: app-bitcoin-new/bin/app.elf bitcoind-builder: name: bitcoind builder - runs-on: ubuntu-latest + # Ubuntu 22.04 ships with glibc 2.35, which is needed to keep binaries + # compatible with Debian Bookworm (glibc 2.36) Python containers. + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-bitcoind - test-dist: - name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + test-trezor-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script }} runs-on: ubuntu-latest - if: ${{ always() }} - needs: [ dist-builder, sim-builder, ledger-app-builder, bitcoind-builder ] + needs: + - dist-builder + - sim-builder-trezor + - bitcoind-builder strategy: fail-fast: false matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] device: - 'trezor-1' - 'trezor-t' + script: + - 'Wheel' + - 'Sdist' + - 'Bindist' + + env: + DEVICE: '--${{ matrix.device }}' + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist + + test-coldcard-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + runs-on: ubuntu-latest + needs: + - dist-builder + - sim-builder-coldcard + - bitcoind-builder + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: - 'coldcard' + script: + - 'Wheel' + - 'Sdist' + - 'Bindist' + + env: + DEVICE: '--${{ matrix.device }}' + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist + + test-bitbox-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + runs-on: ubuntu-latest + needs: + - dist-builder + - sim-builder-bitbox + - bitcoind-builder + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: - 'bitbox01' - - 'jade' - - 'ledger' - - 'ledger-legacy' - - 'keepkey' - 'bitbox02' script: - - name: 'Wheel' - install: 'pip install dist/*.whl' - test: 'cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd ..' - - name: 'Sdist' - install: 'pip install $(find dist -name "*.tar.gz" -a -not -name "*linux*")' - test: 'cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd ..' - - name: 'Bindist' - install: 'poetry install; cd dist; tar -xvf hwi*linux*.tar.gz; cd ..' - test: 'cd test; poetry run ./run_tests.py $DEVICE --interface=bindist --device-only; cd ..' + - 'Wheel' + - 'Sdist' + - 'Bindist' env: DEVICE: '--${{ matrix.device }}' @@ -239,85 +356,213 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist - - run: | - pip install poetry + test-jade-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + runs-on: ubuntu-latest + needs: + - dist-builder + - sim-builder-jade + - bitcoind-builder - - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'jade' + script: + - 'Wheel' + - 'Sdist' + - 'Bindist' - - run: | - ${{ matrix.script.install }} + env: + DEVICE: '--${{ matrix.device }}' - - uses: actions/download-artifact@v4 - with: - name: bitcoind + container: python:${{ matrix.python-version }} - - run: | - tar -xvf bitcoind.tar.gz + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist - - uses: ./.github/actions/install-sim - with: - device: ${{ matrix.device }} + test-ledger-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + runs-on: ubuntu-latest + needs: + - dist-builder + - sim-builder-ledger + - bitcoind-builder - - name: Run tests - run: | - ${{ matrix.script.test }} + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'ledger' + - 'ledger-legacy' + script: + - 'Wheel' + - 'Sdist' + - 'Bindist' - - if: failure() - run: | - tail -v -n +1 test/*.std* + env: + DEVICE: '--${{ matrix.device }}' + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist + + test-keepkey-dist: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.script.name }} + runs-on: ubuntu-latest + needs: + - dist-builder + - sim-builder-keepkey + - bitcoind-builder + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'keepkey' + script: + - 'Wheel' + - 'Sdist' + - 'Bindist' + + env: + DEVICE: '--${{ matrix.device }}' + + container: python:${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-dist - test-all: + test-trezor: name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} runs-on: ubuntu-latest - if: ${{ always() }} - needs: [ sim-builder, ledger-app-builder, bitcoind-builder ] + needs: [ sim-builder-trezor, bitcoind-builder ] timeout-minutes: 45 strategy: fail-fast: false matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] device: - 'trezor-1' - 'trezor-t' - - 'coldcard' - - 'bitbox01' - - 'jade' - - 'ledger' - - 'ledger-legacy' - - 'keepkey' - - 'bitbox02' interface: [ 'library', 'cli', 'stdin' ] container: python:${{ matrix.python-version }} steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device - - run: | - pip install poetry - poetry install - - - uses: actions/download-artifact@v4 - with: - name: bitcoind + test-ledger: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} + runs-on: ubuntu-latest + needs: [ sim-builder-ledger, ledger-s-app-builder, ledger-x-app-builder, bitcoind-builder ] + timeout-minutes: 45 - - run: | - tar -xvf bitcoind.tar.gz + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'ledger' + - 'ledger-legacy' + interface: [ 'library', 'cli', 'stdin' ] - - uses: ./.github/actions/install-sim - with: - device: ${{ matrix.device }} + container: python:${{ matrix.python-version }} - - name: Run tests - run: | - cd test; poetry run ./run_tests.py --${{ matrix.device }} --interface=${{ matrix.interface }} --device-only; cd .. + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device + + test-coldcard: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} + runs-on: ubuntu-22.04 + needs: [ sim-builder-coldcard, bitcoind-builder ] + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'coldcard' + interface: [ 'library', 'cli', 'stdin' ] + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device + + test-bitbox: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} + runs-on: ubuntu-latest + needs: [ sim-builder-bitbox, bitcoind-builder ] + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'bitbox01' + - 'bitbox02' + interface: [ 'library', 'cli', 'stdin' ] + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device + + test-jade: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} + runs-on: ubuntu-latest + needs: [ sim-builder-jade, bitcoind-builder ] + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'jade' + interface: [ 'library', 'cli', 'stdin' ] + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device + + test-keepkey: + name: Python ${{ matrix.python-version }} ${{ matrix.device }} ${{ matrix.interface }} + runs-on: ubuntu-latest + needs: [ sim-builder-keepkey, bitcoind-builder ] + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + device: + - 'keepkey' + interface: [ 'library', 'cli', 'stdin' ] + + container: python:${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test-device - - if: failure() - run: | - tail -v -n +1 test/*.std* diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile index 17d6752d8..07c201c6e 100644 --- a/ci/cirrus.Dockerfile +++ b/ci/cirrus.Dockerfile @@ -1,6 +1,6 @@ # Cache break (modify this line to break cirrus' dockerfile build cache) 1 -FROM python:3.8 +FROM python:3.9 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update @@ -10,7 +10,7 @@ RUN apt-get install -y \ bsdmainutils \ build-essential \ ccache \ - clang \ + clang \ cmake \ curl \ cython3 \ diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index a1edf1171..4e1025352 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster-slim +FROM debian:bookworm-slim SHELL ["/bin/bash", "-c"] diff --git a/docs/examples/bitcoin-core-usage.rst b/docs/examples/bitcoin-core-usage.rst index 5114dc228..cf0170a25 100644 --- a/docs/examples/bitcoin-core-usage.rst +++ b/docs/examples/bitcoin-core-usage.rst @@ -32,7 +32,7 @@ Clone Bitcoin Core and build it. Clone HWI. $ cd bitcoin $ cmake -B build $ cmake --build build - $ build/src/bitcoind -daemon -addresstype=bech32 -changetype=bech32 + $ build/bin/bitcoind -daemon -addresstype=bech32 -changetype=bech32 $ cd .. $ git clone https://github.com/bitcoin-core/HWI.git $ cd HWI diff --git a/hwilib/devices/ckcc/README.md b/hwilib/devices/ckcc/README.md index 8db249b74..0d99aea3d 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 [ca8d2b7808784a9f4927f3250bf52d2623a4e15b](https://github.com/Coldcard/ckcc-protocol/tree/ca8d2b7808784a9f4927f3250bf52d2623a4e15b). +This stripped down version was made at commit [f87d30f220cb6334eb3c4ace93c1b62e04942022](https://github.com/Coldcard/ckcc-protocol/commit/f87d30f220cb6334eb3c4ace93c1b62e04942022). ## Changes diff --git a/hwilib/devices/ckcc/__init__.py b/hwilib/devices/ckcc/__init__.py index b2f0b70ea..91f9ba615 100644 --- a/hwilib/devices/ckcc/__init__.py +++ b/hwilib/devices/ckcc/__init__.py @@ -1,6 +1,4 @@ -__version__ = '1.0.2' - -__all__ = [ "client", "protocol", "constants" ] - +__version__ = '1.4.0' +__all__ = [ "client", "protocol", "constants" ] \ No newline at end of file diff --git a/hwilib/devices/ckcc/client.py b/hwilib/devices/ckcc/client.py index 1f265fb25..9f0ce2d4f 100644 --- a/hwilib/devices/ckcc/client.py +++ b/hwilib/devices/ckcc/client.py @@ -8,27 +8,26 @@ # # - ec_mult, ec_setup, aes_setup, mitm_verify # -import hid, sys, os, platform -from binascii import b2a_hex, a2b_hex +import hid, os, socket, atexit +from binascii import b2a_hex from hashlib import sha256 -from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN +from .constants import USB_NCRY_V1, USB_NCRY_V2 +from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN from .utils import decode_xpub, get_pubkey_string # unofficial, unpermissioned... USB numbers COINKITE_VID = 0xd13e CKCC_PID = 0xcc10 -# Unix domain socket used by the simulator -CKCC_SIMULATOR_PATH = '/tmp/ckcc-simulator.sock' +DEFAULT_SIM_SOCKET = "/tmp/ckcc-simulator.sock" + class ColdcardDevice: - def __init__(self, sn=None, dev=None, encrypt=True): + def __init__(self, sn=None, dev=None, encrypt=True, ncry_ver=USB_NCRY_V1, is_simulator=False): # Establish connection via USB (HID) or Unix Pipe - self.is_simulator = False + self.is_simulator = is_simulator - if not dev and sn and '/' in sn: - if platform.system() == 'Windows': - raise RuntimeError("Cannot connect to simulator. Is it running?") + if not dev and ((sn and ('/' in sn)) or self.is_simulator): dev = UnixSimulatorPipe(sn) found = 'simulator' self.is_simulator = True @@ -49,7 +48,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): break if not dev: - raise KeyError("Could not find Coldcard!" + raise KeyError("Could not find Coldcard!" if not sn else ('Cannot find CC with serial: '+sn)) else: found = dev.get_serial_number_string() @@ -58,6 +57,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.serial = found # they will be defined after we've established a shared secret w/ device + self.ncry_ver = ncry_ver self.session_key = None self.encrypt_request = None self.decrypt_response = None @@ -67,7 +67,7 @@ def __init__(self, sn=None, dev=None, encrypt=True): self.resync() if encrypt: - self.start_encryption() + self.start_encryption(version=self.ncry_ver) def close(self): # close underlying HID device @@ -101,17 +101,21 @@ def send_recv(self, msg, expect_errors=False, verbose=0, timeout=3000, encrypt=T # first byte of each 64-byte packet encodes length or packet-offset assert 4 <= len(msg) <= MAX_MSG_LEN, "msg length: %d" % len(msg) - if not self.encrypt_request: + if self.encrypt_request is None: # disable encryption if not already enabled for this connection encrypt = False + if self.encrypt_request and self.ncry_ver == USB_NCRY_V2: + # ncry version 2 - everything needs to be encrypted + encrypt = True + if encrypt: msg = self.encrypt_request(msg) left = len(msg) offset = 0 while left > 0: - # Note: first byte always zero (HID report number), + # Note: first byte always zero (HID report number), # [1] is framing header (length+flags) # [2:65] payload (63 bytes, perhaps including padding) here = min(63, left) @@ -224,7 +228,7 @@ def aes_setup(self, session_key): 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): + def start_encryption(self, version=USB_NCRY_V1): # setup encryption on the link # - pick our own key pair, IV for AES # - send IV and pubkey to device @@ -233,10 +237,12 @@ def start_encryption(self): pubkey = self.ec_setup() - msg = CCProtocolPacker.encrypt_start(pubkey) + msg = CCProtocolPacker.encrypt_start(pubkey, version=version) his_pubkey, fingerprint, xpub = self.send_recv(msg, encrypt=False) + self.ncry_ver = version + self.session_key = self.ec_mult(his_pubkey) # capture some public details of remote side's master key @@ -248,7 +254,6 @@ def start_encryption(self): self.aes_setup(self.session_key) 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) @@ -318,42 +323,54 @@ def download_file(self, length, checksum, blksize=1024, file_number=1): return data - def hash_password(self, text_password): + def hash_password(self, text_password, v3=False): # Turn text password into a key for use in HSM auth protocol + # - changed from pbkdf2_hmac_sha256 to pbkdf2_hmac_sha512 in version 4 of CC firmware 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) + return pbkdf2_hmac('sha256' if v3 else 'sha512', text_password, salt, PBKDF2_ITER_COUNT)[:32] class UnixSimulatorPipe: # Use a UNIX pipe to the simulator instead of a real USB connection. # - emulates the API of hidapi device object. - def __init__(self, path): - import socket, atexit + def __init__(self, socket_path=None): + self.socket_path = socket_path or DEFAULT_SIM_SOCKET self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) try: - self.pipe.connect(path) + self.pipe.connect(self.socket_path) except Exception: self.close() raise RuntimeError("Cannot connect to simulator. Is it running?") - instance = 0 - while instance < 10: - pn = '/tmp/ckcc-client-%d-%d.sock' % (os.getpid(), instance) + last_err = None + for instance in range(5): + # if simulator has PID in socket path, client will have matching, or empty + pn = '/tmp/ckcc-client%s-%d-%d.sock' % (self.get_sim_pid(), os.getpid(), instance) try: self.pipe.bind(pn) # just needs any name break - except OSError: - instance += 1 + except OSError as err: + last_err = err + if os.path.exists(pn): + os.remove(pn) continue + else: + raise last_err # raise whatever was raised last in the loop self.pipe_name = pn atexit.register(self.close) + def get_sim_pid(self): + # return str PID if any in socket_path + if self.socket_path == DEFAULT_SIM_SOCKET: + return "" + return "-" + self.socket_path.split(".")[0].split("-")[-1] + def read(self, max_count, timeout_ms=None): import socket if not timeout_ms: @@ -383,7 +400,7 @@ def close(self): pass def get_serial_number_string(self): - return 'simulator' + return 'F1'*6 -# EOF +# EOF \ No newline at end of file diff --git a/hwilib/devices/ckcc/constants.py b/hwilib/devices/ckcc/constants.py index ebbd93770..2218011c2 100644 --- a/hwilib/devices/ckcc/constants.py +++ b/hwilib/devices/ckcc/constants.py @@ -6,6 +6,22 @@ except ImportError: const = int +# USB encryption versions (default USB_NCRY_V1) +# +# This introduces a new ncry version to close a potential attack vector: +# +# A malicious program may re-initialize the connection encryption by sending the ncry command a second time during USB operation. +# This may prove particularly harmful in HSM mode. +# +# Sending version USB_NCRY_V2 changes the behavior in two ways: +# * All future commands must be encrypted +# * Returns an error if the ncry command is sent again for the duration of the power cycle +# +# USB_NCRY_V2 is most suitable for HSM mode as in case of any communication issue or simply by closing `ColdcardDevice` +# Coldcard will need to reboot to recover USB operation if USB_NCRY_V2. +USB_NCRY_V1 = const(0x01) +USB_NCRY_V2 = const(0x02) + # For upload/download this is the max size of the data block. MAX_BLK_LEN = const(2048) @@ -17,17 +33,29 @@ # - the max on the wire for mainnet is 100k # - but a PSBT might contain a full txn for each input MAX_TXN_LEN = const(384*1024) +MAX_TXN_LEN_MK4 = const(2*1024*1024) # Max size of any upload (firmware.dfu files in particular) MAX_UPLOAD_LEN = const(2*MAX_TXN_LEN) +MAX_UPLOAD_LEN_MK4 = const(2*MAX_TXN_LEN_MK4) # Max length of text messages for signing MSG_SIGNING_MAX_LENGTH = const(240) +# Bitcoin limitation: max number of signatures in P2SH redeem script (non-segwit) +# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes +# - serializations of M/N in redeem scripts assume this range +MAX_SIGNERS = const(15) +# taproot artificial multisig limit +MAX_TR_SIGNERS = const(34) + +TAPROOT_LEAF_MASK = 0xfe +TAPROOT_LEAF_TAPSCRIPT = 0xc0 + # 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_HMAC = const(3) # PBKDF2('hmac-sha512', scrt, sha256(psbt), PBKDF2_ITER_COUNT)[:32] USER_AUTH_SHOW_QR = const(0x80) # show secret on Coldcard screen (best for TOTP enroll) MAX_USERNAME_LEN = 16 @@ -48,6 +76,7 @@ AFC_BECH32 = const(0x04) # just how we're encoding it? AFC_SCRIPT = const(0x08) # paying into a script AFC_WRAPPED = const(0x10) # for transition/compat types for segwit vs. old +AFC_BECH32M = const(0x20) # no difference between script/key path in taproot # Numeric codes for specific address types AF_CLASSIC = AFC_PUBKEY # 1addr @@ -56,11 +85,13 @@ AF_P2WSH = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32 # segwit multisig AF_P2WPKH_P2SH = AFC_WRAPPED | AFC_PUBKEY | AFC_SEGWIT # looks classic P2SH, but p2wpkh inside AF_P2WSH_P2SH = AFC_WRAPPED | AFC_SCRIPT | AFC_SEGWIT # looks classic P2SH, segwit multisig +AF_P2TR = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32M # bc1p SUPPORTED_ADDR_FORMATS = frozenset([ AF_CLASSIC, AF_P2SH, AF_P2WPKH, + AF_P2TR, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, @@ -68,21 +99,66 @@ # 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) -PSBT_IN_PARTIAL_SIG = const(2) -PSBT_IN_SIGHASH_TYPE = const(3) -PSBT_IN_REDEEM_SCRIPT = const(4) -PSBT_IN_WITNESS_SCRIPT = const(5) -PSBT_IN_BIP32_DERIVATION = const(6) -PSBT_IN_FINAL_SCRIPTSIG = const(7) -PSBT_IN_FINAL_SCRIPTWITNESS = const(8) - -PSBT_OUT_REDEEM_SCRIPT = const(0) -PSBT_OUT_WITNESS_SCRIPT = const(1) -PSBT_OUT_BIP32_DERIVATION = const(2) - -# EOF +# GLOBAL === +PSBT_GLOBAL_UNSIGNED_TX = const(0x00) +PSBT_GLOBAL_XPUB = const(0x01) +PSBT_GLOBAL_VERSION = const(0xfb) +PSBT_GLOBAL_PROPRIETARY = const(0xfc) +# BIP-370 +PSBT_GLOBAL_TX_VERSION = const(0x02) +PSBT_GLOBAL_FALLBACK_LOCKTIME = const(0x03) +PSBT_GLOBAL_INPUT_COUNT = const(0x04) +PSBT_GLOBAL_OUTPUT_COUNT = const(0x05) +PSBT_GLOBAL_TX_MODIFIABLE = const(0x06) + +# INPUTS === +PSBT_IN_NON_WITNESS_UTXO = const(0x00) +PSBT_IN_WITNESS_UTXO = const(0x01) +PSBT_IN_PARTIAL_SIG = const(0x02) +PSBT_IN_SIGHASH_TYPE = const(0x03) +PSBT_IN_REDEEM_SCRIPT = const(0x04) +PSBT_IN_WITNESS_SCRIPT = const(0x05) +PSBT_IN_BIP32_DERIVATION = const(0x06) +PSBT_IN_FINAL_SCRIPTSIG = const(0x07) +PSBT_IN_FINAL_SCRIPTWITNESS = const(0x08) +PSBT_IN_POR_COMMITMENT = const(0x09) +PSBT_IN_RIPEMD160 = const(0x0a) +PSBT_IN_SHA256 = const(0x0b) +PSBT_IN_HASH160 = const(0x0c) +PSBT_IN_HASH256 = const(0x0d) +# BIP-370 +PSBT_IN_PREVIOUS_TXID = const(0x0e) +PSBT_IN_OUTPUT_INDEX = const(0x0f) +PSBT_IN_SEQUENCE = const(0x10) +PSBT_IN_REQUIRED_TIME_LOCKTIME = const(0x11) +PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = const(0x12) +# BIP-371 +PSBT_IN_TAP_KEY_SIG = const(0x13) +PSBT_IN_TAP_SCRIPT_SIG = const(0x14) +PSBT_IN_TAP_LEAF_SCRIPT = const(0x15) +PSBT_IN_TAP_BIP32_DERIVATION = const(0x16) +PSBT_IN_TAP_INTERNAL_KEY = const(0x17) +PSBT_IN_TAP_MERKLE_ROOT = const(0x18) + +# OUTPUTS === +PSBT_OUT_REDEEM_SCRIPT = const(0x00) +PSBT_OUT_WITNESS_SCRIPT = const(0x01) +PSBT_OUT_BIP32_DERIVATION = const(0x02) +# BIP-370 +PSBT_OUT_AMOUNT = const(0x03) +PSBT_OUT_SCRIPT = const(0x04) +# BIP-371 +PSBT_OUT_TAP_INTERNAL_KEY = const(0x05) +PSBT_OUT_TAP_TREE = const(0x06) +PSBT_OUT_TAP_BIP32_DERIVATION = const(0x07) + +RFC_SIGNATURE_TEMPLATE = '''\ +-----BEGIN BITCOIN SIGNED MESSAGE----- +{msg} +-----BEGIN BITCOIN SIGNATURE----- +{addr} +{sig} +-----END BITCOIN SIGNATURE----- +''' + +# EOF \ No newline at end of file diff --git a/hwilib/devices/ckcc/protocol.py b/hwilib/devices/ckcc/protocol.py index 92431910e..54c744e6d 100644 --- a/hwilib/devices/ckcc/protocol.py +++ b/hwilib/devices/ckcc/protocol.py @@ -63,11 +63,15 @@ def check_mitm(): @staticmethod def start_backup(): - # prompts user with password for encrytped backup + # prompts user with password for encrypted backup return b'back' @staticmethod - def encrypt_start(device_pubkey, version=0x1): + def encrypt_start(device_pubkey, version=USB_NCRY_V1): + supported_versions = [USB_NCRY_V1, USB_NCRY_V2] + if version not in supported_versions: + raise ValueError("Unsupported USB encryption version. " + "Supported versions: %s" % (supported_versions)) assert len(device_pubkey) == 64, "want uncompressed 64-byte pubkey, no prefix byte" return pack('<4sI64s', b'ncry', version, device_pubkey) @@ -120,6 +124,36 @@ def multisig_enroll(length, file_sha): assert len(file_sha) == 32 return pack('<4sI32s', b'enrl', length, file_sha) + @staticmethod + def miniscript_ls(): + # list registered miniscript wallet names + return b'msls' + + @staticmethod + def miniscript_delete(name): + # delete registered miniscript wallet by name + assert 2 <= len(name) <= 40, "name len" + return b'msdl' + name.encode('ascii') + + @staticmethod + def miniscript_get(name): + # get registered miniscript wallet object by name + assert 2 <= len(name) <= 40, "name len" + return b'msgt' + name.encode('ascii') + + @staticmethod + def miniscript_address(name, change=False, idx=0): + # get miniscript address from internal or external chain by id + assert 2 <= len(name) <= 40, "name len" + assert 0 <= idx < (2**31), "child idx" + return pack('<4sII', b'msas', int(change), idx) + name.encode('ascii') + + @staticmethod + def miniscript_enroll(length, file_sha): + # miniscript details must already be uploaded as a text file, this starts approval process. + assert len(file_sha) == 32 + return pack('<4sI32s', b'mins', length, file_sha) + @staticmethod def multisig_check(M, N, xfp_xor): # do we have a wallet already that matches M+N and xor(*xfps)? @@ -140,7 +174,7 @@ def show_address(subpath, addr_fmt=AF_CLASSIC): @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 + # 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 @@ -224,76 +258,89 @@ class CCProtocolUnpacker: # - given full rx message to work from # - this is done after un-framing - @classmethod - def decode(cls, msg): + @staticmethod + def decode(msg): assert len(msg) >= 4 sign = str(msg[0:4], 'utf8', 'ignore') - d = getattr(cls, sign, cls) - if d is cls: + d = getattr(CCProtocolUnpacker, sign, None) + if d is None: raise CCFramingError('Unknown response signature: ' + repr(sign)) return d(msg) - # struct info for each response - + + @staticmethod def okay(msg): # trivial response, w/ no content assert len(msg) == 4 return None # low-level errors + @staticmethod def fram(msg): - raise CCFramingError("Framing Error", str(msg[4:], 'utf8')) + raise CCFramingError("Framing Error: " + str(msg[4:], 'utf8')) + + @staticmethod def err_(msg): raise CCProtoError("Coldcard Error: " + str(msg[4:], 'utf8', 'ignore'), msg[4:]) + @staticmethod def refu(msg): # user didn't want to approve something raise CCUserRefused() + @staticmethod def busy(msg): # user didn't want to approve something raise CCBusyError() + @staticmethod def biny(msg): # binary string: length implied by msg framing return msg[4:] + @staticmethod def int1(msg): return unpack_from('= 15 assert len(psbt_sha) == 32 - digest = hmac.new(key, psbt_sha, sha256).digest() + digest = hmac.new(key, psbt_sha, hashlib.sha256).digest() num = struct.unpack('>I', digest[-4:])[0] & 0x7fffffff return '%06d' % (num % 1000000) -# EOF +# EOF \ No newline at end of file diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index e64f3619c..0c6219a90 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -100,6 +100,21 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal device.open_path(path.encode()) self.device = ColdcardDevice(dev=device) + self._is_edge = None + + @property + def is_edge(self): + """ + Cached property, no need to ask device more than once + :return: bool + """ + if self._is_edge is None: + try: + self._is_edge = self.is_edge_firmware() + except: pass # silent fail, normal firmware is implied + + return self._is_edge + @coldcard_exception def get_pubkey_at_path(self, path: str) -> ExtendedKey: self.device.check_mitm() @@ -132,14 +147,15 @@ def sign_tx(self, tx: PSBT) -> PSBT: # 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.fingerprint == master_fp and key not in psbt_in.partial_sigs: - our_keys += 1 - if our_keys > passes: - passes = our_keys + if not self.is_edge: + 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.fingerprint == master_fp and key not in psbt_in.partial_sigs: + our_keys += 1 + if our_keys > passes: + passes = our_keys for _ in range(passes): # Get psbt in hex and then make binary @@ -390,13 +406,27 @@ def toggle_passphrase(self) -> bool: """ raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host') - def can_sign_taproot(self) -> bool: + def firmware_version(self) -> str: + return self.device.send_recv(CCProtocolPacker.version()).split("\n")[1] + + def is_edge_firmware(self): + """ + :return: True if this device is running EDGE firmware, False otherwise """ - The Coldard does not support Taproot yet. + if self.device.is_simulator: + cmd = "import version; RV.write(str(int(getattr(version, 'is_edge', 0))))" + rv = self.device.send_recv(b'EXEC' + cmd.encode('utf-8'), timeout=60000, encrypt=False) + return rv == b"1" + else: + _, ver, _, _, _ = self.device.send_recv(CCProtocolPacker.version()).split("\n") + return "X" == self.firmware_version()[-1] - :returns: False, always + def can_sign_taproot(self) -> bool: + """ + Only COLDCARD EDGE support taproot. + :returns: Whether Taproot is supported """ - return False + return self.is_edge def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain = Chain.MAIN, allow_emulators: bool = True) -> List[Dict[str, Any]]: @@ -422,6 +452,9 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain with handle_errors(common_err_msgs["enumerate"], d_data): try: client = ColdcardClient(path) + if client.is_edge: + d_data['label'] = 'edge' + d_data['model'] = 'edge_' + d_data['model'] d_data['fingerprint'] = client.get_master_fingerprint().hex() except RuntimeError as e: # Skip the simulator if it's not there diff --git a/hwilib/devices/trezorlib/btc.py b/hwilib/devices/trezorlib/btc.py index 1dff24880..96cb3040e 100644 --- a/hwilib/devices/trezorlib/btc.py +++ b/hwilib/devices/trezorlib/btc.py @@ -19,7 +19,6 @@ from decimal import Decimal from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Sequence, Tuple -# TypedDict is not available in typing for python < 3.8 from typing_extensions import TypedDict from . import exceptions, messages diff --git a/poetry.lock b/poetry.lock index 1e1047655..8f1173a36 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, @@ -17,6 +18,7 @@ version = "0.17.4" description = "Python graph (network) package" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, @@ -28,6 +30,7 @@ version = "1.7.0" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"}, {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"}, @@ -43,14 +46,12 @@ version = "2.14.0" description = "Internationalization utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] @@ -60,6 +61,7 @@ version = "5.6.0" description = "CBOR (de)serializer with extensive tag support" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cbor2-5.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7569627514699b10d903795e344e5520cd758f7db968e46e667b6875c409610b"}, {file = "cbor2-5.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e6d5c5b5cb25450561c92c9ac7d72912027cfa8807aab77ea6f5549e2157335"}, @@ -102,7 +104,7 @@ files = [ [package.extras] benchmarks = ["pytest-benchmark (==4.0.0)"] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions ; python_version < \"3.12\""] test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] @@ -111,6 +113,7 @@ version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, @@ -122,6 +125,8 @@ version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, @@ -186,6 +191,7 @@ version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["dev"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -285,6 +291,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -296,6 +304,7 @@ version = "42.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, @@ -350,6 +359,7 @@ version = "0.18.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, @@ -361,6 +371,7 @@ version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, @@ -379,6 +390,7 @@ version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, @@ -395,6 +407,7 @@ version = "0.14.0" description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "hidapi-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f68bbf88805553911e7e5a9b91136c96a54042b6e3d82d39d733d2edb46ff9a6"}, {file = "hidapi-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b264c6a1a1a0cacacc82299785415bec91184cb3e4a77d127c40016086705327"}, @@ -476,6 +489,7 @@ version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, @@ -487,6 +501,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -498,6 +513,8 @@ version = "7.0.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, @@ -509,7 +526,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "jinja2" @@ -517,6 +534,7 @@ version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, @@ -534,6 +552,7 @@ version = "3.1.0" description = "Pure-python wrapper for libusb-1.0" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "libusb1-3.1.0-py3-none-any.whl", hash = "sha256:9d9f16e2c199cab91f48ead585d3f5ec7e8e4be428a25ddfed22abf786fa9b3a"}, {file = "libusb1-3.1.0-py3-none-win32.whl", hash = "sha256:bc7874302565721f443a27d8182fcc7152e5b560523f12f1377b130f473e4a0c"}, @@ -547,6 +566,8 @@ version = "1.16.3" description = "Mach-O header analysis and editing" optional = false python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" files = [ {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, @@ -561,6 +582,7 @@ version = "2.1.4" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, @@ -630,6 +652,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -641,6 +664,7 @@ version = "0.20" description = "Implementation of Bitcoin BIP-0039" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "mnemonic-0.20-py3-none-any.whl", hash = "sha256:acd2168872d0379e7a10873bb3e12bf6c91b35de758135c4fbd1015ef18fafc5"}, {file = "mnemonic-0.20.tar.gz", hash = "sha256:7c6fb5639d779388027a77944680aee4870f0fcd09b1e42a5525ee2ce4c625f6"}, @@ -652,6 +676,7 @@ version = "0.991" description = "Optional static typing for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, @@ -702,6 +727,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -713,6 +739,7 @@ version = "0.3.1" description = "Implementation of Noise Protocol Framework" optional = false python-versions = "~=3.5" +groups = ["main"] files = [ {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, {file = "noiseprotocol-0.3.1.tar.gz", hash = "sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645"}, @@ -727,6 +754,7 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -738,6 +766,8 @@ version = "2023.2.7" description = "Python PE parsing module" optional = false python-versions = ">=3.6.0" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, @@ -749,6 +779,7 @@ version = "4.25.2" description = "" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, @@ -769,6 +800,7 @@ version = "1.6.1" description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, ] @@ -779,6 +811,7 @@ version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, @@ -790,6 +823,8 @@ version = "2.21" description = "C parser in Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -801,6 +836,7 @@ version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, @@ -812,13 +848,14 @@ version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] -plugins = ["importlib-metadata"] +plugins = ["importlib-metadata ; python_version < \"3.8\""] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -827,6 +864,7 @@ version = "6.3.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false python-versions = "<3.13,>=3.8" +groups = ["dev"] files = [ {file = "pyinstaller-6.3.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:75a6f2a6f835a2e6e0899d10e60c10caf5defd25aced38b1dd48fbbabc89de07"}, {file = "pyinstaller-6.3.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:de25beb176f73a944758553caacec46cc665bf3910ad8a174706d79cf6e95340"}, @@ -862,6 +900,7 @@ version = "2024.0" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyinstaller-hooks-contrib-2024.0.tar.gz", hash = "sha256:a7118c1a5c9788595e5c43ad058a7a5b7b6d59e1eceb42362f6ec1f0b61986b0"}, {file = "pyinstaller_hooks_contrib-2024.0-py2.py3-none-any.whl", hash = "sha256:469b5690df53223e2e8abffb2e44d6ee596e7d79d4b1eed9465123b67439875a"}, @@ -878,6 +917,7 @@ version = "3.5" description = "Python Serial Port Extension" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, @@ -892,6 +932,8 @@ version = "5.15.2.1" description = "Python bindings for the Qt cross-platform application and UI framework" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" +groups = ["main"] +markers = "python_version == \"3.9\" and extra == \"qt\"" files = [ {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:b5e1d92f26b0bbaefff67727ccbb2e1b577f2c0164b349b3d6e80febb4c5bde2"}, {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:235240b6ec8206d9fdf0232472c6ef3241783d480425e5b54796f06e39ed23da"}, @@ -904,23 +946,14 @@ files = [ [package.dependencies] shiboken2 = "5.15.2.1" -[[package]] -name = "pytz" -version = "2023.4" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.4-py2.py3-none-any.whl", hash = "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a"}, - {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, -] - [[package]] name = "pywin32-ctypes" version = "0.2.2" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, @@ -932,6 +965,7 @@ version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, @@ -953,6 +987,7 @@ version = "3.0.2" description = "Python helper for Semantic Versioning (https://semver.org)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, @@ -964,6 +999,7 @@ version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, @@ -971,7 +1007,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -980,6 +1016,8 @@ version = "5.15.2.1" description = "Python / C++ bindings helper module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" +groups = ["main"] +markers = "python_version == \"3.9\" and extra == \"qt\"" files = [ {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:f890f5611ab8f48b88cfecb716da2ac55aef99e2923198cefcf781842888ea65"}, {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87079c07587859a525b9800d60b1be971338ce9b371d6ead81f15ee5a46d448b"}, @@ -995,6 +1033,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1006,6 +1045,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -1017,6 +1057,7 @@ version = "7.1.2" description = "Python documentation generator" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, @@ -1052,6 +1093,7 @@ version = "1.3.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["dev"] files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, @@ -1071,6 +1113,7 @@ version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, @@ -1086,6 +1129,7 @@ version = "0.1.8" description = "Documenting CLI programs" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "sphinxcontrib-autoprogram-0.1.8.tar.gz", hash = "sha256:5a69729db9d283e0e4c6d349bd60e62a4b8ebd2c07c0ab634b82d08a4121f10a"}, {file = "sphinxcontrib_autoprogram-0.1.8-py2.py3-none-any.whl", hash = "sha256:222b029217b05cb22a6c72a473bafd0e57bbe666420be636d91e5ea71b704610"}, @@ -1101,6 +1145,7 @@ version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {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"}, @@ -1116,6 +1161,7 @@ version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, @@ -1131,6 +1177,7 @@ version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" +groups = ["dev"] files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, @@ -1145,6 +1192,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {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"}, @@ -1159,6 +1207,7 @@ version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {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"}, @@ -1174,6 +1223,7 @@ version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, @@ -1189,6 +1239,7 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1200,6 +1251,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1211,6 +1264,7 @@ version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, @@ -1222,13 +1276,14 @@ version = "2.2.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1239,6 +1294,8 @@ version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, @@ -1246,12 +1303,12 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff"] [extras] qt = ["pyside2"] [metadata] -lock-version = "2.0" -python-versions = "^3.8,<3.13" -content-hash = "a24d5b8abbe7db4818f283f1adab4667c7255f427b86bc96ae88e539691a3947" +lock-version = "2.1" +python-versions = "^3.9,<3.13" +content-hash = "536fcc537f47e6fd969f84474533853a87cfc8b60613b7dea5f88ecbea8365d0" diff --git a/pyproject.toml b/pyproject.toml index a3a69cd48..76d8f09ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.8,<3.13" +python = "^3.9,<3.13" hidapi = ">=0.14.0" ecdsa = "~0" pyaes = "^1.6" @@ -34,7 +34,7 @@ protobuf = "^4.23.3" [tool.poetry.extras] qt = ["pyside2"] -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pyinstaller = "^6.3" autopep8 = "~1" flake8 = ">=3" diff --git a/setup.py b/setup.py index 13fa4a924..47bef782d 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, - 'python_requires': '>=3.8,<3.13', + 'python_requires': '>=3.9,<3.13', } diff --git a/test/README.md b/test/README.md index c3efb9262..1e1264ec7 100644 --- a/test/README.md +++ b/test/README.md @@ -293,7 +293,7 @@ cmake gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gdb-multiarch qemu-user-stat The python packages can be installed with ``` -pip install construct flask-restful jsonschema mnemonic pyelftools pillow requests +pip install construct flask-cors flask-restful jsonschema mnemonic pyelftools pillow requests ``` ### Building diff --git a/test/data/coldcard-multisig.patch b/test/data/coldcard-multisig.patch index 23038b66f..b9ad6477c 100644 --- a/test/data/coldcard-multisig.patch +++ b/test/data/coldcard-multisig.patch @@ -1,34 +1,3 @@ -From c2e4c24c226555903705aae0386aefe0e15bf873 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/variant/pyb.py | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/unix/variant/pyb.py b/unix/variant/pyb.py -index d22bb1b..fe8e7ca 100644 ---- a/unix/variant/pyb.py -+++ b/unix/variant/pyb.py -@@ -36,10 +36,10 @@ class USB_HID: - import usocket as socket - self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - # If on linux, try commenting the following line -- addr = bytes([len(self.fn)+2, socket.AF_UNIX] + list(self.fn)) -+ # addr = bytes([len(self.fn)+2, socket.AF_UNIX] + list(self.fn)) - # If on linux, try uncommenting the following two lines -- #import struct -- #addr = struct.pack('H108s', socket.AF_UNIX, self.fn) -+ import struct -+ addr = struct.pack('H108s', socket.AF_UNIX, self.fn) - while 1: - try: - self.pipe.bind(addr) --- -2.38.1 - - From fd51e85693e0d66129133b1f195134aead1cf7d0 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 17 Dec 2019 17:56:05 -0500 @@ -58,21 +27,21 @@ index 2706fb4..f9b533d 100644 -- 2.38.1 +From 8b4323c1e393d79d46248dd822ca9aaaeb2b2bc3 Mon Sep 17 00:00:00 2001 +From: Sjors Provoost +Date: Wed, 23 Jul 2025 10:16:22 +0200 +Subject: [PATCH] Allow multisigs to share master fingerprint -From 04aecb1005d997783e63bd6cd830c4a98f099cb8 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 - +Co-Authored-By: Ava Chow --- - shared/multisig.py | 40 +++++++++++++++++++++++++--------------- - 1 file changed, 25 insertions(+), 15 deletions(-) + shared/multisig.py | 37 ++++++++++++++++++++++++------------- + 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/shared/multisig.py b/shared/multisig.py -index 6f26644..6e190b5 100644 +index 446998a2..cabb2003 100644 --- a/shared/multisig.py +++ b/shared/multisig.py -@@ -144,9 +144,9 @@ class MultisigWallet: +@@ -144,9 +144,9 @@ class MultisigWallet(WalletABC): # calc useful cache value: numeric xfp+subpath, with lookup self.xfp_paths = {} for xfp, deriv, xpub in self.xpubs: @@ -84,7 +53,7 @@ index 6f26644..6e190b5 100644 @classmethod def render_addr_fmt(cls, addr_fmt): -@@ -245,7 +245,11 @@ class MultisigWallet: +@@ -270,7 +270,11 @@ class MultisigWallet(WalletABC): def get_xfp_paths(self): # return list of lists [xfp, *deriv] @@ -97,7 +66,7 @@ index 6f26644..6e190b5 100644 @classmethod def find_match(cls, M, N, xfp_paths, addr_fmt=None): -@@ -280,24 +284,30 @@ class MultisigWallet: +@@ -305,24 +309,31 @@ class MultisigWallet(WalletABC): # the same prefix path per-each xfp, as indicated # xfp_paths (unordered)? # - could also check non-prefix part is all non-hardened @@ -110,22 +79,16 @@ index 6f26644..6e190b5 100644 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 -+ + +- 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 @@ -137,9 +100,14 @@ index 6f26644..6e190b5 100644 + break + else: + # No match was found + +- 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)) return False return True -- -2.38.1 +2.39.5 (Apple Git-154) diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 694cde276..0e53a29d4 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|The change path|Change path|external inputs|Register wallet|Policy map|Key|Path|Public key|Spend from|Wallet name|Wallet policy).*", + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if you're|The change path|Change path|external inputs|Register wallet|Register account|Policy map|Key|Path|Public key|Spend from|Account name|Wallet name|Wallet policy|Descriptor template).*", "actions": [ [ "button", 2, true ], [ "button", 2, false ] diff --git a/test/run_tests.py b/test/run_tests.py index cd502a6c0..42384b117 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -66,7 +66,7 @@ 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') +parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/build/bin/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") diff --git a/test/setup_environment.sh b/test/setup_environment.sh index cb4600fe8..b35bb24c8 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -110,6 +110,7 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then rustup toolchain install nightly rustup default nightly rustup component add rustfmt + rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu # 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 @@ -248,7 +249,7 @@ if [[ -n ${build_keepkey} ]]; then fi if [[ -n ${build_ledger} ]]; then - speculos_packages="construct flask-restful jsonschema mnemonic pyelftools pillow requests pytesseract" + speculos_packages="construct flask-cors flask-restful jsonschema mnemonic pyelftools pillow requests pytesseract" poetry run pip install ${speculos_packages} pip install ${speculos_packages} # Clone ledger simulator Speculos if it doesn't exist, or update it if it does @@ -305,54 +306,11 @@ if [[ -n ${build_jade} ]]; then git submodule update --recursive --init fi - # Deduce the relevant versions of esp-idf and qemu to use + # Deduce the relevant version of esp-idf to use ESP_IDF_BRANCH=$(grep "ARG ESP_IDF_BRANCH=" Dockerfile | cut -d\= -f2) ESP_IDF_COMMIT=$(grep "ARG ESP_IDF_COMMIT=" Dockerfile | cut -d\= -f2) - ESP_QEMU_BRANCH=$(grep "ARG ESP_QEMU_BRANCH=" Dockerfile | cut -d\= -f2) - ESP_QEMU_COMMIT=$(grep "ARG ESP_QEMU_COMMIT=" Dockerfile | cut -d\= -f2) cd .. - # Build the qemu emulator if required - - # If the directory exists, see if it is at the expected commit - # If not, remove the entire directory (it will be re-cloned below) - if [ -d "qemu" ]; then - cd qemu - LOCAL=$(git rev-parse @) - if [ $LOCAL = $ESP_QEMU_COMMIT ]; then - echo "esp-qemu up-to-date" - cd .. - else - cd .. - rm -fr qemu - fi - fi - - # Clone the upstream if the directory does not exist - # Then build the emulator - if [ ! -d "qemu" ]; then - git clone --quiet --depth 1 --branch ${ESP_QEMU_BRANCH} --single-branch --shallow-submodules https://github.com/espressif/qemu.git ./qemu - cd qemu - - git checkout ${ESP_QEMU_COMMIT} - ./configure \ - --target-list=xtensa-softmmu \ - --enable-gcrypt \ - --disable-sanitizers \ - --disable-strip \ - --disable-user \ - --disable-capstone \ - --disable-vnc \ - --disable-sdl \ - --disable-gtk \ - --enable-slirp \ - --extra-cflags=-Wno-array-parameter - ninja -C build - cd .. - fi - - # Build the esp-idf toolchain if required - # We will install the esp-idf tools in a given location (otherwise defaults to user home dir) export IDF_TOOLS_PATH="$(pwd)/esp-idf-tools" @@ -394,6 +352,13 @@ if [[ -n ${build_jade} ]]; then # Export the tools . ./esp-idf/export.sh + # Install the emulator + idf_tools.py install qemu-xtensa + QEMU_EXE=$(find ${IDF_TOOLS_PATH} -type f -name qemu-system-xtensa) + QEMU_BIOS_DIR=$(dirname "${QEMU_EXE}")/../share/qemu + echo "Installed qemu emulator: ${QEMU_EXE}" + echo "Installed qemu bios files: ${QEMU_BIOS_DIR}" + # Build Blockstream Jade firmware configured for the emulator cd jade rm -fr sdkconfig @@ -412,8 +377,8 @@ if [[ -n ${build_jade} ]]; then # Extract the minimal artifacts required to run the emulator rm -fr simulator mkdir simulator - cp qemu/build/qemu-system-xtensa simulator/ - cp -R qemu/pc-bios simulator/ + cp ${QEMU_EXE} simulator/ + cp -R ${QEMU_BIOS_DIR} simulator/pc-bios cp jade/main/qemu/flash_image.bin simulator/ cp jade/main/qemu/qemu_efuse.bin simulator/ diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index fb9cf8490..c1cd7f63f 100644 --- a/test/test_bitbox02.py +++ b/test/test_bitbox02.py @@ -51,6 +51,10 @@ def start(self): ) time.sleep(1) + # Prevent CI from lingering until timeout: + if self.simulator_proc.poll() is not None: + raise RuntimeError(f"BitBox02 simulator failed with exit code {self.simulator_proc.poll()}") + self.setup_client = Bitbox02Client(self.path) self.setup_bb02 = self.setup_client.restore_device() self.setup_client.close() diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 9eb90f379..0369497e5 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -53,7 +53,7 @@ def start(self): self.coldcard_proc = subprocess.Popen( [ "python3", - os.path.basename(self.simulator), "--ms" + os.path.basename(self.simulator), "--ms", "--headless" ], cwd=os.path.dirname(self.simulator), stdout=self.coldcard_log, @@ -61,6 +61,10 @@ def start(self): ) # Wait for simulator to be up while True: + # Prevent CI from lingering until timeout: + if self.coldcard_proc.poll() is not None: + raise RuntimeError(f"coldcard simulator failed with exit code {self.coldcard_proc.poll()}") + try: enum_res = process_commands(["--emulators", "enumerate"]) found = False diff --git a/test/test_device.py b/test/test_device.py index 887a6ea65..b682ac57b 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -96,6 +96,10 @@ def get_free_port(): cookie_path = os.path.join(self.datadir, "regtest", ".cookie") while not os.path.exists(cookie_path): time.sleep(0.5) + # Prevent CI from lingering until timeout: + if self.bitcoind_proc.poll() is not None: + raise RuntimeError(f"bitcoind failed with exit code {self.bitcoind_proc.poll()}") + # Read .cookie file to get user and pass with open(cookie_path) as f: self.userpass = f.readline().lstrip().rstrip() @@ -133,7 +137,7 @@ def create(*args, **kwargs): return c class DeviceTestCase(unittest.TestCase): - def __init__(self, bitcoind, emulator=None, interface='library', methodName='runTest'): + def __init__(self, bitcoind, emulator: DeviceEmulator = None, interface='library', methodName='runTest'): super(DeviceTestCase, self).__init__(methodName) self.bitcoind = bitcoind self.rpc = bitcoind.rpc @@ -160,17 +164,17 @@ def do_command(self, args): cli_args.append(shlex.quote(arg)) if self.interface == 'cli': proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, shell=True) - result = proc.communicate() + result = proc.communicate(timeout=60) return json.loads(result[0].decode()) elif self.interface == 'bindist': proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, shell=True) - result = proc.communicate() + result = proc.communicate(timeout=60) 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) - result = proc.communicate(input_str.encode()) + result = proc.communicate(input_str.encode(), timeout=60) return json.loads(result[0].decode()) else: return process_commands(args) @@ -579,12 +583,20 @@ def _test_signtx(self, input_types, multisig_types, external, op_return: bool): # Test wrapper to avoid mixed-inputs signing for Ledger def test_signtx(self): + if self.emulator.type == "ledger" and not self.emulator.legacy: + # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 + raise unittest.SkipTest("Test temporarily disabled for NanoX") + for addrtypes, multisig_types, external, op_return in self.signtx_cases: with self.subTest(addrtypes=addrtypes, multisig_types=multisig_types, external=external, op_return=op_return): self._test_signtx(addrtypes, multisig_types, external, op_return) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): + if self.emulator.type == "ledger" and not self.emulator.legacy: + # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 + raise unittest.SkipTest("Test temporarily disabled for NanoX") + # make a huge transaction keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "sh_wit", "0", "100"]) import_result = self.wrpc.importdescriptors(keypool_desc) @@ -615,6 +627,10 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def test_display_address_path(self): + if self.emulator.type == "ledger" and not self.emulator.legacy: + # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 + raise unittest.SkipTest("Test temporarily disabled for NanoX") + result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "legacy", '--path', 'm/44h/1h/0h/0/0']) if self.emulator.supports_legacy: self.assertNotIn('error', result) @@ -640,6 +656,10 @@ def test_display_address_bad_path(self): self.assertEqual(result['code'], -7) def test_display_address_descriptor(self): + if self.emulator.type == "ledger" and not self.emulator.legacy: + # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 + raise unittest.SkipTest("Test temporarily disabled for NanoX") + account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub'] p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub'] legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub'] @@ -761,6 +781,10 @@ def _check_sign_msg(self, msg): self.assertTrue(self.rpc.verifymessage(addr, sig, msg)) def test_sign_msg(self): + if self.emulator.type == "ledger" and not self.emulator.legacy: + # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 + raise unittest.SkipTest("Test temporarily disabled for NanoX") + self._check_sign_msg("Message signing test") self._check_sign_msg("285") # Specific test case for Ledger shorter S diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index d43a9af65..1a98c9562 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -58,6 +58,10 @@ def start(self): ) # Wait for simulator to be up while True: + # Prevent CI from lingering until timeout: + if self.simulator_proc.poll() is not None: + raise RuntimeError(f"BitBox simulator failed with exit code {self.simulator_proc.poll()}") + try: self.dev = BitboxSimulator('127.0.0.1', 35345) reply = send_plain(b'{"password":"0000"}', self.dev) diff --git a/test/test_jade.py b/test/test_jade.py index 1e707dfe2..71422eab7 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -82,6 +82,10 @@ def start(self): # Wait for emulator to be up while True: + # Prevent CI from lingering until timeout: + if self.emulator_proc.poll() is not None: + raise RuntimeError(f"Jade simulator failed with exit code {self.emulator_proc.poll()}") + time.sleep(1) try: # Try to connect and set the test seed diff --git a/test/test_keepkey.py b/test/test_keepkey.py index f926d9807..845a0976a 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -77,6 +77,10 @@ def start(self): sock.connect(('127.0.0.1', 11044)) sock.settimeout(0) while True: + # Prevent CI from lingering until timeout: + if self.emulator_proc.poll() is not None: + raise RuntimeError(f"Keepkey simulator failed with exit code {self.emulator_proc.poll()}") + try: sock.sendall(b"PINGPING") r = sock.recv(8) diff --git a/test/test_ledger.py b/test/test_ledger.py index 26876c0df..c63bb5853 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -52,7 +52,7 @@ def start(self): super().start() automation_path = os.path.abspath("data/speculos-automation.json") app_path = "./apps/nanos#btc#2.0#ce796c1b.elf" if self.legacy else "./apps/btc-test.elf" - os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.0" if self.legacy else "Bitcoin Test:2.1.0" + os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.0" if self.legacy else "Bitcoin Test:2.4.1" self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator @@ -70,7 +70,7 @@ def start(self): 'seproxyhal:DEBUG', '--api-port', '0', - '--model', 'nanos', + '--model', 'nanos' if self.legacy else 'nanox', app_path ], cwd=os.path.dirname(self.emulator_path), @@ -79,6 +79,10 @@ def start(self): ) # Wait for simulator to be up while True: + # Prevent CI from lingering until timeout: + if self.emulator_proc.poll() is not None: + raise RuntimeError(f"Ledger simulator failed with exit code {self.emulator_proc.poll()}") + try: enum_res = process_commands(["--emulators", "enumerate"]) found = False diff --git a/test/test_trezor.py b/test/test_trezor.py index f7424ce90..335fa571f 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -76,6 +76,10 @@ def start(self): sock.connect(('127.0.0.1', 21324)) sock.settimeout(0) while True: + # Prevent CI from lingering until timeout: + if self.emulator_proc.poll() is not None: + raise RuntimeError(f"Trezor simulator failed with exit code {self.emulator_proc.poll()}") + try: sock.sendall(b"PINGPING") r = sock.recv(8)