diff --git a/.github/actions/build-bitcoind/action.yml b/.github/actions/build-bitcoind/action.yml index e4befd42e..4fc591cf3 100644 --- a/.github/actions/build-bitcoind/action.yml +++ b/.github/actions/build-bitcoind/action.yml @@ -31,7 +31,7 @@ runs: path: ${{ env.CCACHE_DIR }} key: ${{ github.job }}-ccache-${{ github.run_id }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: bitcoind path: bitcoind.tar.gz diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml index 2968a9f40..ebc6c9897 100644 --- a/.github/actions/build-sim/action.yml +++ b/.github/actions/build-sim/action.yml @@ -16,8 +16,13 @@ runs: - 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 libltdl-dev + apt_options=( + -o Acquire::Retries=3 + -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30 + ) + timeout 5m sudo apt-get "${apt_options[@]}" update + timeout 30m sudo apt-get "${apt_options[@]}" install -y gcc-arm-linux-gnueabihf libsdl2-image-dev libslirp-dev libpcsclite-dev ninja-build libltdl-dev pip install poetry uv 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 @@ -46,7 +51,7 @@ runs: done tar -czf "${{ inputs.archive }}.tar.gz" ${{ inputs.paths }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ${{ inputs.name }}-sim path: ${{ inputs.archive }}.tar.gz diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index f6630e6f1..6e18f6fc2 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -7,7 +7,7 @@ inputs: runs: using: composite steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: ${{ inputs.device == 'ledger-legacy' && 'ledger' || inputs.device }}-sim @@ -18,18 +18,28 @@ runs: apt-get install -y libsdl2-image-2.0-0 libusb-1.0-0 tar -xvf trezor-firmware.tar.gz - - if: inputs.device == 'coldcard' + - if: startsWith(inputs.device, 'coldcard') shell: bash run: | apt-get update apt-get install -y libpcsclite-dev libusb-1.0-0 swig + archive=coldcard-firmware + if [[ "${{ inputs.device }}" == "coldcard-edge" ]]; then + archive=coldcard-edge-firmware + fi # Extract the archive - this includes the full firmware directory - tar -xvf coldcard-firmware.tar.gz + tar -xvf "${archive}.tar.gz" - poetry run pip install -r test/work/firmware/requirements.txt - pip install -r test/work/firmware/requirements.txt + requirements="test/work/firmware/requirements.txt" + if [[ "${{ inputs.device }}" == "coldcard-edge" ]]; then + # Edge's full firmware requirements pin legacy development tools that + # are not needed to run the simulator and do not build on modern Python. + requirements="test/work/firmware/unix/requirements.txt" + fi + poetry run pip install -r "${requirements}" poetry run pip install pysdl2-dll + pip install -r "${requirements}" pip install pysdl2-dll - if: inputs.device == 'bitbox01' @@ -63,7 +73,7 @@ runs: pip install -e test/work/speculos - if: startsWith(inputs.device, 'ledger') - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.device == 'ledger-legacy' && 'ledger_app_legacy' || 'ledger_app' }} diff --git a/.github/actions/test-device/action.yml b/.github/actions/test-device/action.yml index 2fc72beda..2152d1343 100644 --- a/.github/actions/test-device/action.yml +++ b/.github/actions/test-device/action.yml @@ -8,7 +8,7 @@ runs: pip install poetry poetry install - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: bitcoind diff --git a/.github/actions/test-dist/action.yml b/.github/actions/test-dist/action.yml index 6675fe34e..a517c0bb0 100644 --- a/.github/actions/test-dist/action.yml +++ b/.github/actions/test-dist/action.yml @@ -7,7 +7,7 @@ runs: run: | pip install poetry - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: dist path: dist/ @@ -30,7 +30,7 @@ runs: run: | poetry install; cd dist; tar -xvf hwi*linux*.tar.gz; cd .. - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: bitcoind diff --git a/.github/sim-build-map.json b/.github/sim-build-map.json index c5ad49fb0..5d83fc4c0 100644 --- a/.github/sim-build-map.json +++ b/.github/sim-build-map.json @@ -6,6 +6,9 @@ "coldcard": [ { "name": "coldcard", "archive": "coldcard-firmware", "paths": "test/work/firmware" } ], + "coldcard_edge": [ + { "name": "coldcard-edge", "archive": "coldcard-edge-firmware", "paths": "test/work/firmware" } + ], "bitbox": [ { "name": "bitbox01", "archive": "mcu", "paths": "test/work/mcu" }, { "name": "bitbox02", "archive": "bitbox02", "paths": "test/work/bitbox02-firmware/build-build-noasan/bin/simulator" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91b8fd52c..cb66eea51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest container: python:3.12 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - run: | pip install poetry @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest container: python:3.12 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - run: | pip install poetry @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: docker/build-push-action@v6 with: @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: docker/build-push-action@v6 with: @@ -98,7 +98,7 @@ jobs: find dist -type f -exec sha256sum {} \; " - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: dist path: dist/ @@ -125,6 +125,15 @@ jobs: include: ${{ needs.prepare-sim-matrices.outputs.coldcard }} runs-on: ubuntu-latest + sim-builder-coldcard-edge: + name: Coldcard Edge sim builder + needs: prepare-sim-matrices + uses: ./.github/workflows/sim-builder.yml + with: + sim: coldcard-edge + include: ${{ needs.prepare-sim-matrices.outputs.coldcard_edge }} + runs-on: ubuntu-latest + sim-builder-bitbox: name: Bitbox sim builder needs: prepare-sim-matrices @@ -177,7 +186,7 @@ jobs: name: bitcoind builder runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: ./.github/actions/build-bitcoind test-trezor-1: @@ -215,6 +224,14 @@ jobs: device: coldcard runs-on: ubuntu-latest + test-coldcard-edge: + uses: ./.github/workflows/device-test.yml + needs: [sim-builder-coldcard-edge, bitcoind-builder, dist-builder] + with: + device: coldcard-edge + runs-on: ubuntu-latest + python_versions: '["3.14"]' + test-bitbox01: uses: ./.github/workflows/device-test.yml needs: [sim-builder-bitbox, bitcoind-builder, dist-builder] diff --git a/.github/workflows/device-test.yml b/.github/workflows/device-test.yml index 2aff297a0..ccfd477c0 100644 --- a/.github/workflows/device-test.yml +++ b/.github/workflows/device-test.yml @@ -9,6 +9,10 @@ on: required: false type: string default: ubuntu-latest + python_versions: + required: false + type: string + default: '["3.10", "3.11", "3.12", "3.13", "3.14"]' jobs: test-device: @@ -18,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ${{ fromJSON(inputs.python_versions) }} device: - ${{ inputs.device }} test: @@ -30,7 +34,7 @@ jobs: - {script: 'Bindist'} container: python:${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: run test if: ${{ matrix.test.interface }} uses: ./.github/actions/test-device diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index 19cb9cd0a..d2cab7345 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -19,7 +19,7 @@ jobs: git clone --branch 2.5.0 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git cd app-bitcoin make DEBUG=1 COIN=bitcoin_testnet BOLOS_SDK=$NANOX_SDK - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ledger_app path: app-bitcoin/bin/app.elf diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml index 28d7a2d45..821696621 100644 --- a/.github/workflows/ledger-legacy-app-builder.yml +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -19,7 +19,7 @@ jobs: git clone --depth 1 https://github.com/LedgerHQ/app-bitcoin-legacy.git -b legacy-1.6.6 cd app-bitcoin-legacy make DEBUG=1 BOLOS_SDK=$NANOSP_SDK - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ledger_app_legacy path: app-bitcoin-legacy/bin/app.elf diff --git a/.github/workflows/prepare-sim-matrices.yml b/.github/workflows/prepare-sim-matrices.yml index 4f2d71841..3e30277e3 100644 --- a/.github/workflows/prepare-sim-matrices.yml +++ b/.github/workflows/prepare-sim-matrices.yml @@ -8,6 +8,9 @@ on: coldcard: description: JSON include array for coldcard value: ${{ jobs.prepare.outputs.coldcard }} + coldcard_edge: + description: JSON include array for Coldcard Edge + value: ${{ jobs.prepare.outputs.coldcard_edge }} bitbox: description: JSON include array for bitbox value: ${{ jobs.prepare.outputs.bitbox }} @@ -28,19 +31,20 @@ jobs: outputs: trezor: ${{ steps.gen.outputs.trezor }} coldcard: ${{ steps.gen.outputs.coldcard }} + coldcard_edge: ${{ steps.gen.outputs.coldcard_edge }} bitbox: ${{ steps.gen.outputs.bitbox }} jade: ${{ steps.gen.outputs.jade }} ledger: ${{ steps.gen.outputs.ledger }} keepkey: ${{ steps.gen.outputs.keepkey }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - id: gen shell: bash run: | set -euo pipefail sudo apt-get install -y jq map_file=".github/sim-build-map.json" - for sim in trezor coldcard bitbox jade ledger keepkey; do + for sim in trezor coldcard coldcard_edge bitbox jade ledger keepkey; do include=$(jq -c --arg s "$sim" '.[$s]' "$map_file") if [[ -z "$include" || "$include" == "null" ]]; then echo "Missing entry for $sim in $map_file" >&2 diff --git a/.github/workflows/sim-builder.yml b/.github/workflows/sim-builder.yml index 708ec61b8..b2b09a56f 100644 --- a/.github/workflows/sim-builder.yml +++ b/.github/workflows/sim-builder.yml @@ -22,7 +22,7 @@ jobs: matrix: include: ${{ fromJSON(inputs.include) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: ./.github/actions/build-sim with: name: ${{ matrix.name }} diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml index 09a04c08c..679844c08 100644 --- a/.github/workflows/type-check.yml +++ b/.github/workflows/type-check.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest container: python:3.12 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - run: | pip install poetry diff --git a/hwilib/devices/coldcard.py b/hwilib/devices/coldcard.py index 4f5e5586d..8b627e5f6 100644 --- a/hwilib/devices/coldcard.py +++ b/hwilib/devices/coldcard.py @@ -43,6 +43,7 @@ AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH, + AF_P2TR, ) from .._base58 import ( get_xpub_fingerprint, @@ -79,6 +80,15 @@ # Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md +def _firmware_version_supports_psbt_v2(version: str) -> bool: + if version.endswith(("Q", "X")): + return True + try: + return tuple(int(part) for part in version.split(".")) >= (5, 2, 0) + except ValueError: + return False + + def coldcard_exception(f: Callable[..., Any]) -> Callable[..., Any]: @wraps(f) def func(*args: Any, **kwargs: Any) -> Any: @@ -122,6 +132,12 @@ def is_edge(self): return self._is_edge + def _supports_psbt_v2(self) -> bool: + if self.device.is_simulator or self.is_edge: + return True + version = self.device.firmware_version()[1] + return _firmware_version_supports_psbt_v2(version) + @coldcard_exception def get_pubkey_at_path(self, path: str) -> ExtendedKey: self.device.check_mitm() @@ -164,9 +180,11 @@ def sign_tx(self, tx: PSBT) -> PSBT: if our_keys > passes: passes = our_keys + if tx.version == 2 and not self._supports_psbt_v2(): + tx.convert_to_v0() + for _ in range(passes): # Get psbt in hex and then make binary - tx.convert_to_v0() fd = io.BytesIO(base64.b64decode(tx.serialize())) # learn size (portable way) @@ -267,7 +285,9 @@ def display_singlesig_address( elif addr_type == AddressType.LEGACY: addr_fmt = AF_CLASSIC elif addr_type == AddressType.TAP: - raise UnavailableActionError("Coldcard does not support displaying Taproot addresses yet") + if not self.is_edge: + raise UnavailableActionError("Coldcard does not support displaying Taproot addresses yet. Use EDGE.") + addr_fmt = AF_P2TR else: raise BadArgumentError("Unknown address type") diff --git a/test/data/coldcard-edge-multisig.patch b/test/data/coldcard-edge-multisig.patch new file mode 100644 index 000000000..29cca3cdf --- /dev/null +++ b/test/data/coldcard-edge-multisig.patch @@ -0,0 +1,10 @@ +diff --git a/unix/variant/sim_settings.py b/unix/variant/sim_settings.py +--- a/unix/variant/sim_settings.py ++++ b/unix/variant/sim_settings.py +@@ -71,5 +71,5 @@ if '--ms' in sys.argv: + sim_defaults['miniscript'] = [['CC-2-of-4', 'sh(wsh(sortedmulti(2,@0/**,@1/**,@2/**,@3/**)))', ['[0f056943/48h/1h/0h/1h]tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP', '[6ba6cfd0/48h/1h/0h/1h]tpubDFcrvj5n7gyatVbr8dHCUfHT4CGvL8hREBjtxc4ge7HZgqNuPhFimPRtVg6fRRwfXiQthV9EBjNbwbpgV2VoQeL1ZNXoAWXxP2L9vMtRjax', '[747b698e/48h/1h/0h/1h]tpubDExj5FnaUnPAjjgzELoSiNRkuXJG8Cm1pbdiA4Hc5vkAZHphibeVcUp6mqH5LuNVKbtLVZxVSzyja5X26Cfmx6pzRH6gXBUJAH7MiqwNyuM', '[7bb026be/48h/1h/0h/1h]tpubDFiuHYSJhNbHaGtB5skiuDLg12tRboh2uVZ6KGXxr8WVr28pLcS7F3gv8SsHFa2tm1jtx3VAuw56YfgRkdo6DXyfp51oygTKY3nJFT5jBMt'], {'af': 26, 'm_n': (2, 4), 'b67': 1, 'ct': 'XTN'}]] + else: + # P2SH: 2of4 using BIP39 passwords: "Me", "Myself", "and I", and (empty string) on simulator +- sim_defaults['miniscript'] = [['MeMyself', 'sh(sortedmulti(2,@0/**,@1/**,@2/**,@3/**))', ['[6ba6cfd0/45h]tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9', '[747b698e/45h]tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc', '[7bb026be/45h]tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa', '[0f056943/45h]tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n'], {'af': 8, 'm_n': (2, 4), 'b67': 1, 'ct': 'XTN'}]] ++ sim_defaults['miniscript'] = [['mstest', 'sh(sortedmulti(2,@0/**,@1/**,@2/**,@3/**))', ['[6ba6cfd0/45h]tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9', '[747b698e/45h]tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc', '[7bb026be/45h]tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa', '[0f056943/45h]tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n'], {'af': 8, 'm_n': (2, 4), 'b67': 1, 'ct': 'XTN'}]] + sim_defaults['fee_limit'] = -1 diff --git a/test/run_tests.py b/test/run_tests.py index 80300db1f..28d8f7a95 100755 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -7,7 +7,7 @@ from test_base58 import TestBase58 from test_bech32 import TestSegwitAddress from test_bip32 import TestBIP32 -from test_coldcard import coldcard_test_suite +from test_coldcard import coldcard_test_suite, TestColdcardFirmware from test_descriptor import TestDescriptor from test_device import Bitcoind from test_psbt import TestPSBT @@ -32,6 +32,10 @@ coldcard_group.add_argument('--no-coldcard', dest='coldcard', help='Do not run Coldcard test with simulator', action='store_false') coldcard_group.add_argument('--coldcard', dest='coldcard', help='Run Coldcard test with simulator', action='store_true') +coldcard_edge_group = parser.add_mutually_exclusive_group() +coldcard_edge_group.add_argument('--no-coldcard-edge', dest='coldcard_edge', help='Do not run Coldcard Edge test with simulator', action='store_false') +coldcard_edge_group.add_argument('--coldcard-edge', dest='coldcard_edge', help='Run Coldcard Edge test with simulator', action='store_true') + ledger_group = parser.add_mutually_exclusive_group() ledger_group.add_argument('--no-ledger', dest='ledger', help='Do not run Ledger test with emulator', action='store_false') ledger_group.add_argument('--ledger', dest='ledger', help='Run Ledger test with emulator', action='store_true') @@ -59,6 +63,7 @@ parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf') parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh') parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcard simulator', default='work/firmware/unix/simulator.py') +parser.add_argument('--coldcard-edge-path', dest='coldcard_edge_path', help='Path to Coldcard Edge simulator', default='work/firmware/unix/simulator.py') parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu') parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator') parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py') @@ -71,7 +76,7 @@ parser.add_argument("--device-only", help="Only run device tests", action="store_true") -parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None) +parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, coldcard_edge=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None) args = parser.parse_args() @@ -84,6 +89,7 @@ suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBase58)) suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestBIP32)) + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestColdcardFirmware)) if sys.platform.startswith("linux"): suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller)) success = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite).wasSuccessful() @@ -93,6 +99,7 @@ args.trezor_1 = True if args.trezor_1 is None else args.trezor_1 args.trezor_t = True if args.trezor_t is None else args.trezor_t args.coldcard = True if args.coldcard is None else args.coldcard + args.coldcard_edge = True if args.coldcard_edge is None else args.coldcard_edge args.keepkey = True if args.keepkey is None else args.keepkey args.bitbox01 = True if args.bitbox01 is None else args.bitbox01 args.ledger = True if args.ledger is None else args.ledger @@ -104,6 +111,7 @@ args.trezor_1 = False if args.trezor_1 is None else args.trezor_1 args.trezor_t = False if args.trezor_t is None else args.trezor_t args.coldcard = False if args.coldcard is None else args.coldcard + args.coldcard_edge = False if args.coldcard_edge is None else args.coldcard_edge args.keepkey = False if args.keepkey is None else args.keepkey args.bitbox01 = False if args.bitbox01 is None else args.bitbox01 args.ledger = False if args.ledger is None else args.ledger @@ -111,7 +119,7 @@ args.jade = False if args.jade is None else args.jade args.bitbox02 = False if args.bitbox02 is None else args.bitbox02 -if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02: +if args.trezor_1 or args.trezor_t or args.coldcard or args.coldcard_edge or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02: # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) @@ -119,6 +127,8 @@ success &= digitalbitbox_test_suite(args.bitbox01_path, bitcoind, args.interface) if success and args.coldcard: success &= coldcard_test_suite(args.coldcard_path, bitcoind, args.interface) + if success and args.coldcard_edge: + success &= coldcard_test_suite(args.coldcard_edge_path, bitcoind, args.interface, is_edge=True) if success and args.trezor_1: success &= trezor_test_suite(args.trezor_1_path, bitcoind, args.interface, '1') if success and args.trezor_t: diff --git a/test/setup_environment.sh b/test/setup_environment.sh index ac76242d9..bec5755b3 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -14,6 +14,10 @@ while [[ $# -gt 0 ]]; do build_coldcard=1 shift ;; + --coldcard-edge) + build_coldcard_edge=1 + shift + ;; --bitbox01) build_bitbox01=1 shift @@ -72,8 +76,8 @@ KEEPKEY_VERSION="v7.10.0" SPECULOS_VERSION="ed952a54801f59a71399462b5422976d84c817bb" # Requires Python >=3.10 (v0.25.11+) JADE_VERSION="1.0.36" -# Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml -COLDCARD_VERSION="2025-09-30T1238-v5.4.4" +COLDCARD_VERSION="2026-07-31T0519-v5.6.0" +COLDCARD_EDGE_VERSION="2026-07-31T1609-v6.6.0X" if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then # Clone trezor-firmware if it doesn't exist, or update it if it does @@ -137,52 +141,61 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then cd .. fi -if [[ -n ${build_coldcard} ]]; then - # Clone coldcard firmware if it doesn't exist, or update it if it does - coldcard_setup_needed=false - if [ ! -d "firmware" ]; then - # Note: cannot use --shallow-submodules because lwip submodule on git.savannah.gnu.org doesn't support it - git clone --recursive --depth 1 --branch ${COLDCARD_VERSION} https://github.com/Coldcard/firmware.git - cd firmware - coldcard_setup_needed=true - else - cd firmware - git reset --hard HEAD~3 # Undo git-am for checking and updating - git fetch - - # Determine if we need to pull. From https://stackoverflow.com/a/3278427 - UPSTREAM=${1:-'@{u}'} - LOCAL=$(git rev-parse @) - REMOTE=$(git rev-parse "$UPSTREAM") - BASE=$(git merge-base @ "$UPSTREAM") - - if [ $LOCAL = $REMOTE ]; then - echo "Up-to-date" - elif [ $LOCAL = $BASE ]; then - git pull +if [[ -n ${build_coldcard} || -n ${build_coldcard_edge} ]]; then + do_coldcard_firmware() { + local coldcard_version="$1" + local coldcard_dir="$2" + local coldcard_requirements="$3" + local coldcard_multisig_patch="$4" + + # Clone coldcard firmware if it doesn't exist, or update it if it does + coldcard_setup_needed=false + if [ ! -d "${coldcard_dir}" ]; then + git clone --depth 1 --branch "${coldcard_version}" https://github.com/Coldcard/firmware.git "${coldcard_dir}" + cd "${coldcard_dir}" + # Simulator setup initializes the required nested dependencies. + git submodule update --init + coldcard_setup_needed=true + else + cd "${coldcard_dir}" + git fetch --tags origin + git checkout --force "${coldcard_version}" + git submodule update --init --force coldcard_setup_needed=true fi - fi - # Apply patch to make simulator work in linux environments - git am ../../data/coldcard-multisig.patch - # Build the simulator. This is cached, but it is also fast - poetry run pip install -r requirements.txt - pip install -r requirements.txt - cd unix - if [ "$coldcard_setup_needed" == true ] ; then - pushd ../external/micropython - # Apply Ubuntu 24.04 compiler warning fixes (included in ColdCard firmware v5.4.4+) - git apply ../../ubuntu24_mpy.patch - popd - pushd ../external/micropython/mpy-cross/ + # Add multisig fixtures used by the Coldcard multisig display tests. + git apply "../../data/${coldcard_multisig_patch}" + + # Build the simulator. This is cached, but it is also fast + poetry run pip install -r "${coldcard_requirements}" + pip install -r "${coldcard_requirements}" + cd unix + if [ "$coldcard_setup_needed" == true ] ; then + pushd ../external/micropython + # Do not treat new warnings from Ubuntu 24.04's compiler as errors. + git apply ../../ubuntu24_mpy.patch + popd + pushd ../external/micropython/mpy-cross/ + make + popd + # Skip make setup, which builds MicroPython's bundled libffi and + # fails with current Autoconf. The simulator links system libffi. + # Initialize its only required MicroPython submodule directly. + git -C ../external/micropython submodule update --init lib/berkeley-db-1.xx + make ngu-setup + ln -sf ../external/micropython/ports/unix/coldcard-mpy . + fi make - popd - make setup - make ngu-setup + cd ../.. + } + + if [[ -n ${build_coldcard} ]]; then + do_coldcard_firmware "${COLDCARD_VERSION}" firmware requirements.txt coldcard-multisig.patch + fi + if [[ -n ${build_coldcard_edge} ]]; then + do_coldcard_firmware "${COLDCARD_EDGE_VERSION}" firmware unix/requirements.txt coldcard-edge-multisig.patch fi - make - cd ../.. fi if [[ -n ${build_bitbox01} ]]; then diff --git a/test/test_coldcard.py b/test/test_coldcard.py index ca64aa523..2c2adf064 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -11,6 +11,8 @@ import unittest from hwilib._cli import process_commands +from hwilib import _bech32 as bech32 +from hwilib.devices.coldcard import _firmware_version_supports_psbt_v2 from test_device import ( Bitcoind, DeviceEmulator, @@ -24,8 +26,27 @@ TestSignTx, ) + +class TestColdcardFirmware(unittest.TestCase): + def test_psbt_v2_support(self): + versions = { + "5.1.4": False, + "5.2.0": True, + "5.6.0": True, + "1.0.0Q": True, + "6.6.0X": True, + "unknown": False, + } + for version, expected in versions.items(): + with self.subTest(version=version): + self.assertEqual( + _firmware_version_supports_psbt_v2(version), + expected, + ) + + class ColdcardSimulator(DeviceEmulator): - def __init__(self, simulator): + def __init__(self, simulator, is_edge=False): try: os.unlink("coldcard-emulator.stdout") except FileNotFoundError: @@ -38,10 +59,10 @@ def __init__(self, simulator): self.fingerprint = "0f056943" self.master_xpub = "tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB" self.password = "" - self.supports_ms_display = True + self.supports_ms_display = not is_edge self.supports_xpub_ms_display = False self.supports_unsorted_ms = False - self.supports_taproot = False + self.supports_taproot = is_edge self.strict_bip48 = False self.include_xpubs = False self.supports_device_multiple_multisig = True @@ -143,13 +164,29 @@ def test_getxpub(self): self.assertEqual(result['chaincode'], '806b26507824f73bc331494afe122f428ef30dde80b2c1ce025d2d03aff411e7') self.assertEqual(result['pubkey'], '0368000bdff5e0b71421c37b8514de8acd4d98ba9908d183d9da56d02ca4fcfd08') -def coldcard_test_suite(simulator, bitcoind, interface): - dev_emulator = ColdcardSimulator(simulator) +class TestColdcardEdgeDisplayAddress(DeviceTestCase): + def test_display_taproot_address(self): + descriptors = self.do_command(self.dev_args + [ + 'getkeypool', '--addr-type', 'tap', '0', '0' + ]) + expected = self.rpc.deriveaddresses(descriptors[0]['desc'], [0, 0])[0] + + result = self.do_command(self.dev_args + [ + 'displayaddress', '--addr-type', 'tap', '--path', 'm/86h/1h/0h/0/0' + ]) + self.assertNotIn('error', result) + self.assertIn('address', result) + displayed_witness = bech32.decode('tb', result['address']) + expected_witness = bech32.decode('bcrt', expected) + self.assertEqual(displayed_witness, expected_witness) + +def coldcard_test_suite(simulator, bitcoind, interface, is_edge=False): + dev_emulator = ColdcardSimulator(simulator, is_edge) signtx_cases = [ - (["legacy"], ["legacy"], False, False), - (["segwit"], ["segwit"], False, False), - (["legacy", "segwit"], ["legacy", "segwit"], False, False), + (["legacy"], [], False, False), + (["segwit"], [], False, False), + (["legacy", "segwit"], [], False, False), ] # Generic device tests @@ -164,6 +201,8 @@ def coldcard_test_suite(simulator, bitcoind, interface): suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface)) suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases)) suite.addTest(DeviceTestCase.parameterize(TestRegisterDescriptor, bitcoind, emulator=dev_emulator, interface=interface, returns_registration=False)) + if is_edge: + suite.addTest(DeviceTestCase.parameterize(TestColdcardEdgeDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface)) result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite) return result.wasSuccessful() @@ -173,9 +212,10 @@ def coldcard_test_suite(simulator, bitcoind, interface): parser.add_argument('simulator', help='Path to the Coldcard simulator') parser.add_argument('bitcoind', help='Path to bitcoind binary') parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library') + parser.add_argument('--edge', help='Test Coldcard Edge behavior', action='store_true') args = parser.parse_args() # Start bitcoind bitcoind = Bitcoind.create(args.bitcoind) - sys.exit(not coldcard_test_suite(args.simulator, bitcoind, args.interface)) + sys.exit(not coldcard_test_suite(args.simulator, bitcoind, args.interface, is_edge=args.edge)) diff --git a/test/test_device.py b/test/test_device.py index 0b0fe45ba..5a55185a4 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -591,10 +591,6 @@ 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 == "coldcard": - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Coldcard sign test temporarily disabled") - 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)