python client: fix three PSBTv2 defects in psbt.py
What changed, and why it matters
This commit fixes three bugs in a Python helper that converts modern PSBTv2 transaction data into the older PSBTv0 format used by Ledger hardware wallets. The bugs could silently corrupt the transaction's lock time and version fields during conversion, which might alter when a transaction becomes valid or what transaction format the device signs. The fixes ensure the correct values are copied before they are erased. There is no direct evidence this was exploitable as a security attack, but it could cause unexpected or invalid signatures in wallet software relying on this library.
Update the Python client library to include this commit. Review any signed transactions produced with the buggy version to confirm nLockTime and version fields match intent, especially for time-locked or version-sensitive transactions. Consider adding regression tests for PSBTv2 deserialization and conversion.
Security signals we found
Silent data corruption in transaction serialization (nLockTime, tx_version, fallback_locktime)
PSBTv2 to PSBTv0 conversion path affected
Potential for producing an unsigned transaction that does not match the PSBT's declared fields
No explicit security advisory or CVE referenced in commit
Evidence from the diff
The patch corrects three defects in bitcoin_client/ledger_bitcoin/psbt.py. (1) get_unsigned_tx() was assigning the computed lock time to self.nLockTime (which does not exist on the PSBT object) instead of tx.nLockTime, so returned CTransaction objects always had nLockTime=0. (2) cache_unsigned_tx_pieces() tested self.tx is not None, but init always creates a CTransaction, so for a deserialized PSBTv2 with tx=null it would call setup_from_tx and overwrite tx_version and fallback_locktime with defaults (1 and 0). The condition now checks self.version == 0. (3) convert_to_v0() cleared tx_version and fallback_locktime before calling get_unsigned_tx(), so the unsigned tx was built from erased defaults; it now builds the tx first, then strips the v2 fields. These are data-integrity bugs in PSBT conversion logic that could affect the transaction the Ledger device is asked to sign.
Changed components
bitcoin_client/ledger_bitcoin/psbt.pyPSBTv2 to PSBTv0 conversion logicLedger Bitcoin app Python client libraryInspect captured patch +6 / −3
### bitcoin_client/ledger_bitcoin/psbt.py
@@ -1051,7 +1051,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):
@@ -1121,7 +1121,7 @@ def get_unsigned_tx(self) -> CTransaction:
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
@@ -1163,14 +1163,17 @@ def convert_to_v0(self) -> None:
Sets this PSBT to version 0
"""
+ # Build the unsigned transaction before stripping the v2 fields
+ tx = self.get_unsigned_tx()
+
if self.version == 2:
# strip PSBT version 2 fields
self.tx_version = None
self.fallback_locktime = None
self.tx_modifiable = None
self._convert_version(0)
- self.tx = self.get_unsigned_tx()
+ self.tx = tx
self.explicit_version = False
Why this scored 48/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.