Merge bitcoin-core/HWI#849: coldcard: bump firmware, add edge, display taproot address, restore test_signtx (ex. multisig)
What changed, and why it matters
This is a routine feature-and-testing update for the Coldcard hardware wallet support in Bitcoin Core's HWI tool. It adds support for Coldcard's 'Edge' experimental firmware, lets users display single-signature Taproot addresses on Edge, and re-enables transaction-signing tests by dropping multisig cases for Coldcard. It also changes how PSBT version 2 is handled: newer Coldcard firmware keeps PSBTv2, while older firmware is downgraded to PSBTv0 before signing. There is no obvious security bug in the diff, but the PSBT version handling and new firmware support are worth a careful look because mistakes there could affect transaction validity or compatibility.
No immediate action required. Treat as a normal feature/test update. Reviewers should verify that the PSBTv2 downgrade path is only taken when the device genuinely does not support v2, and that the version parsing heuristic does not misclassify future firmware. Users of Coldcard Edge should confirm Taproot address display behavior matches expectations.
Security signals we found
PSBT version downgrade logic moved from per-pass to once-per-signing; intended to preserve v2 when supported and downgrade when not
New firmware version parsing heuristic treats Q/X suffixes and Edge/simulator as PSBTv2 capable
Taproot singlesig address display enabled only for Coldcard Edge firmware
Coldcard transaction signing tests re-enabled by removing multisig test cases
CI artifact action version bumps (upload-artifact v4→v7, download-artifact v4→v8, checkout v4→v7)
Evidence from the diff
The merge commit updates CI, test setup, and the Coldcard device driver. Key code changes in hwilib/devices/coldcard.py: (1) adds AF_P2TR import and a _firmware_version_supports_psbt_v2() helper that treats Q/X suffix versions and simulators/Edge as PSBTv2-capable, and requires >=5.2.0 for others; (2) in sign_tx(), if the PSBT is version 2 and the device does not support it, convert_to_v0() is called once before the signing loop instead of on every pass; (3) display_singlesig_address() now allows AddressType.TAPTR when is_edge, using AF_P2TR. Tests are updated to add an Edge simulator, bump firmware to 5.6.0, re-enable test_signtx without multisig, and add a taproot display test for Edge. CI actions are bumped to newer versions and APT commands get timeouts/retries.
Changed components
hwilib/devices/coldcard.pytest/test_coldcard.pytest/test_device.pytest/setup_environment.shtest/run_tests.pytest/data/coldcard-edge-multisig.patch.github/workflows/*.yml.github/actions/*.ymlInspect captured patch +220 / −88
### .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
### .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
### .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' }}
### .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
### .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
### .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" }
### .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]
### .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
### .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
### .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
### .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
### .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 }}
### .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
### 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")
### 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
### 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,21 +111,24 @@
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
args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy
args.jade = False if args.jade is None else args.jade
args.bitbox02 = False if args.bitbox02 is None else args.bitbox02
-if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade 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)
if success and args.bitbox01:
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:
### 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
### 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))
### 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)Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.