nit: tx nVersion serialization is signed integer
What changed, and why it matters
This commit fixes a small data-formatting inconsistency in how the COLDCARD firmware reads and writes the transaction version number inside PSBT v2 files. The code now treats the version as a signed 32-bit integer, matching the Bitcoin protocol, instead of an unsigned 32-bit integer. The practical security impact is likely low because the value is still 4 bytes on the wire and the same bits are produced for all common positive version values. It mainly matters if someone deliberately uses a negative version number, which is unusual and would previously have been misinterpreted as a very large positive number.
Treat as a minor correctness fix. Review whether any downstream logic depends on txn_version being unsigned (e.g., comparisons or version-feature gating) and verify that the change does not alter behavior for valid version 1/2 transactions. No urgent action is required unless the firmware accepts PSBTs from untrusted sources with crafted negative version numbers.
Security signals we found
Data-type mismatch between protocol spec and implementation
PSBT v2 transaction version parsed as unsigned instead of signed integer
Potential misinterpretation of negative nVersion values
No explicit security claim or CVE in commit message
Evidence from the diff
In shared/psbt.py the PSBT_GLOBAL_TX_VERSION field is now unpacked/packed with Python’s struct format ‘<i’ (signed int32) rather than ‘<I’ (unsigned int32). BIP 370 (PSBT v2) and Bitcoin transaction serialization define nVersion as a signed 4-byte little-endian integer. For version values 1 and 2 (the only values in normal use) the wire encoding is identical between signed and unsigned, so the change is behaviorally equivalent in practice. The difference only appears for values whose high bit is set: unsigned parsing would treat 0xFFFFFFFF as 4294967295, while signed parsing treats it as -1. The patch is a one-line read and one-line write correction.
Changed components
shared/psbt.pyPSBT_GLOBAL_TX_VERSION parsing and serializationPSBT v2 handlingInspect captured patch +2 / −2
diff --git a/shared/psbt.py b/shared/psbt.py
index d0ea6e9..18997df 100644
--- a/shared/psbt.py
+++ b/shared/psbt.py
@@ -1067,7 +1067,7 @@ class psbtObject(psbtProxy):
elif kt == PSBT_GLOBAL_VERSION:
self.version = unpack("<I", self.get(val))[0]
elif kt == PSBT_GLOBAL_TX_VERSION:
- self.txn_version = unpack("<I", self.get(val))[0]
+ self.txn_version = unpack("<i", self.get(val))[0]
self.has_gtv = True
elif kt == PSBT_GLOBAL_FALLBACK_LOCKTIME:
self.fallback_locktime = unpack("<I", self.get(val))[0]
@@ -1875,7 +1875,7 @@ class psbtObject(psbtProxy):
wr(PSBT_GLOBAL_UNSIGNED_TX, self.txn)
if self.is_v2:
- wr(PSBT_GLOBAL_TX_VERSION, pack('<I', self.txn_version))
+ wr(PSBT_GLOBAL_TX_VERSION, pack('<i', self.txn_version))
if self.fallback_locktime is not None:
wr(PSBT_GLOBAL_FALLBACK_LOCKTIME, pack('<I', self.fallback_locktime))
wr(PSBT_GLOBAL_INPUT_COUNT, ser_compact_size(self.num_inputs))
Why this scored 29/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.