Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ledger-app-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
steps:
- run: |
# Pin to v2.4.1 - last version that worked with HWI CI (PR #795 merged Sept 2025)
git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin-new.git
cd app-bitcoin-new
git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git
cd app-bitcoin
make DEBUG=1 BOLOS_SDK=$NANOX_SDK
- uses: actions/upload-artifact@v4
with:
name: ledger_app
path: app-bitcoin-new/bin/app.elf
path: app-bitcoin/bin/app.elf
6 changes: 3 additions & 3 deletions .github/workflows/ledger-legacy-app-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
steps:
- run: |
# Pin to legacy-1.6.6 HEAD commit for reproducibility
git clone --depth 1 https://github.com/LedgerHQ/app-bitcoin.git -b legacy-1.6.6
cd app-bitcoin
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
with:
name: ledger_app_legacy
path: app-bitcoin/bin/app.elf
path: app-bitcoin-legacy/bin/app.elf
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bitcoin Hardware Wallet Interface

[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)
[![CI](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml/badge.svg)](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml)
[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)

The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.
Expand Down
99 changes: 0 additions & 99 deletions ci/cirrus.Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion ci/py310.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Cache break (modify this line to break cirrus' dockerfile build cache) 1
# Cache break (modify this line to break the dockerfile build cache) 1

FROM python:3.10

Expand Down
2 changes: 1 addition & 1 deletion ci/py311.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Cache break (modify this line to break cirrus' dockerfile build cache)
# Cache break (modify this line to break the dockerfile build cache)

FROM python:3.11

Expand Down
23 changes: 0 additions & 23 deletions ci/py37.Dockerfile

This file was deleted.

4 changes: 2 additions & 2 deletions hwilib/devices/ledger_bitcoin/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Ledger Bitcoin application client

This is a stripped down version of the client provided at https://github.com/LedgerHQ/app-bitcoin-new/tree/master/bitcoin_client.
This is a stripped down version of the client provided at https://github.com/LedgerHQ/app-bitcoin/tree/master/bitcoin_client.

This stripped down version was made at commit [4e82e44ecfe4ba358da9848087e7e597309abc53](https://github.com/LedgerHQ/app-bitcoin-new/commit/4e82e44ecfe4ba358da9848087e7e597309abc53)
This stripped down version was made at commit [4e82e44ecfe4ba358da9848087e7e597309abc53](https://github.com/LedgerHQ/app-bitcoin/commit/4e82e44ecfe4ba358da9848087e7e597309abc53)

## Changes

Expand Down
48 changes: 36 additions & 12 deletions hwilib/psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ def deserialize(self, f: Readable) -> None:
witness_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore
self.final_script_witness.deserialize(witness_bytes)
elif key_type == PartiallySignedInput.PSBT_IN_PREVIOUS_TXID:
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_PREVIOUS_TXID is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input previous txid is already provided")
elif len(key) != 1:
Expand All @@ -256,6 +258,8 @@ def deserialize(self, f: Readable) -> None:
raise PSBTSerializationError("Previous txid is not 32 bytes")
self.prev_txid = txid
elif key_type == PartiallySignedInput.PSBT_IN_OUTPUT_INDEX:
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_OUTPUT_INDEX is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input previous output index is already provided")
elif len(key) != 1:
Expand All @@ -265,7 +269,8 @@ def deserialize(self, f: Readable) -> None:
raise PSBTSerializationError("Previous output index is not 4 bytes")
self.prev_out = struct.unpack("<I", v)[0]
elif key_type == PartiallySignedInput.PSBT_IN_SEQUENCE:
pass
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_SEQUENCE is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input sequence is already provided")
elif len(key) != 1:
Expand All @@ -275,7 +280,8 @@ def deserialize(self, f: Readable) -> None:
raise PSBTSerializationError("Input sequence is not 4 bytes")
self.sequence = struct.unpack("<I", v)[0]
elif key_type == PartiallySignedInput.PSBT_IN_REQUIRED_TIME_LOCKTIME:
pass
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_REQUIRED_TIME_LOCKTIME is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input required time based locktime is already provided")
elif len(key) != 1:
Expand All @@ -284,8 +290,11 @@ def deserialize(self, f: Readable) -> None:
if len(v) != 4:
raise PSBTSerializationError("Input time based locktime is not 4 bytes")
self.time_locktime = struct.unpack("<I", v)[0]
if self.time_locktime < 500000000:
raise PSBTSerializationError("Input time based locktime is less than 500000000")
elif key_type == PartiallySignedInput.PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
pass
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input required height based locktime index is already provided")
elif len(key) != 1:
Expand All @@ -294,6 +303,8 @@ def deserialize(self, f: Readable) -> None:
if len(v) != 4:
raise PSBTSerializationError("Input height based locktime is not 4 bytes")
self.height_locktime = struct.unpack("<I", v)[0]
if self.height_locktime == 0 or self.height_locktime >= 500000000:
raise PSBTSerializationError("Input height based locktime is not greater than 0 and less than 500000000")
elif key_type == PartiallySignedInput.PSBT_IN_TAP_KEY_SIG:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input Taproot key signature already provided")
Expand Down Expand Up @@ -619,6 +630,8 @@ def deserialize(self, f: Readable) -> None:
elif key_type == PartiallySignedOutput.PSBT_OUT_BIP32_DERIVATION:
DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66])
elif key_type == PartiallySignedOutput.PSBT_OUT_AMOUNT:
if self.version == 0:
raise PSBTSerializationError("PSBT_OUT_AMOUNT is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, output amount already provided")
elif len(key) != 1:
Expand All @@ -628,6 +641,8 @@ def deserialize(self, f: Readable) -> None:
raise PSBTSerializationError("Output amount is not 8 bytes")
self.amount = struct.unpack("<q", v)[0]
elif key_type == PartiallySignedOutput.PSBT_OUT_SCRIPT:
if self.version == 0:
raise PSBTSerializationError("PSBT_OUT_SCRIPT is not allowed in PSBTv0")
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, output script already provided")
elif len(key) != 1:
Expand Down Expand Up @@ -809,6 +824,7 @@ def deserialize(self, psbt: str) -> None:

input_count = None
output_count = None
found_unsigned_tx = False

# Read loop
while True:
Expand All @@ -835,7 +851,13 @@ def deserialize(self, psbt: str) -> None:

# read in value
tx_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore
self.tx.deserialize(tx_bytes)
try:
self.tx.deserialize_without_witness(tx_bytes)
except Exception:
raise PSBTSerializationError("Global unsigned tx is invalid")
if len(tx_bytes.read(1)) > 0:
raise PSBTSerializationError("Global unsigned tx is not serialized without witness")
found_unsigned_tx = True

# Make sure that all scriptSigs and scriptWitnesses are empty
for txin in self.tx.vin:
Expand Down Expand Up @@ -905,7 +927,7 @@ def deserialize(self, psbt: str) -> None:
# Check PSBT version constraints
if self.version == 0:
# make sure that we got an unsigned tx
if self.tx.is_null():
if not found_unsigned_tx:
raise PSBTSerializationError("No unsigned transaction was provided")
# Make sure no v2 fields are present
if self.tx_version is not None:
Expand All @@ -931,7 +953,7 @@ def deserialize(self, psbt: str) -> None:
if output_count is None:
raise PSBTSerializationError("PSBT_GLOBAL_OUTPUT_COUNT is required in PSBTv2")
# Unsigned tx is disallowed
if not self.tx.is_null():
if found_unsigned_tx:
raise PSBTSerializationError("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2")

# Read input data
Expand Down Expand Up @@ -1047,7 +1069,7 @@ def cache_unsigned_tx_pieces(self) -> None:
"""
# To make things easier, we split up the global transaction
# and use the PSBTv2 fields for PSBTv0
if self.tx is not None:
if self.version == 0:
self.setup_from_tx(self.tx)

def setup_from_tx(self, tx: CTransaction):
Expand Down Expand Up @@ -1110,21 +1132,22 @@ def get_unsigned_tx(self) -> CTransaction:

:return: A CTransaction
"""
if not self.tx.is_null():
if self.version == 0:
return self.tx

assert self.tx_version is not None

tx = CTransaction()
tx.nVersion = self.tx_version
self.nLockTime = self.compute_lock_time()
tx.nLockTime = self.compute_lock_time()

for psbt_in in self.inputs:
assert psbt_in.prev_txid is not None
assert psbt_in.prev_out is not None
assert psbt_in.sequence is not None

txin = CTxIn(COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), b"", psbt_in.sequence)
# If omitted, the sequence number is assumed to be the final sequence number
sequence = psbt_in.sequence if psbt_in.sequence is not None else 0xffffffff
txin = CTxIn(COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), b"", sequence)
tx.vin.append(txin)

for psbt_out in self.outputs:
Expand Down Expand Up @@ -1153,6 +1176,7 @@ def convert_to_v0(self) -> None:
"""
Sets this PSBT to version 0
"""
tx = self.get_unsigned_tx()
self._convert_version(0)
self.tx = self.get_unsigned_tx()
self.tx = tx
self.explicit_version = False
9 changes: 9 additions & 0 deletions hwilib/tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ def deserialize(self, f: Readable) -> None:
self.sha256 = None
self.hash = None

def deserialize_without_witness(self, f: Readable) -> None:
self.nVersion = struct.unpack("<i", f.read(4))[0]
self.vin = deser_vector(f, CTxIn)
self.vout = deser_vector(f, CTxOut)
self.wit = CTxWitness()
self.nLockTime = struct.unpack("<I", f.read(4))[0]
self.sha256 = None
self.hash = None

def serialize_without_witness(self) -> bytes:
r = b""
r += struct.pack("<i", self.nVersion)
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ libusb1 = ">=1.7,<4"
pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" }
cbor2 = ">=5.4.6,<5.8"
pyserial = "^3.5"
dataclasses = {version = "^0.8", python = ">=3.6,<3.7"}
semver = "^3.0.1"
noiseprotocol = "^0.3.1"
protobuf = "^4.23.3"
Expand Down
Loading
Loading