psbt: enable type checking
What changed, and why it matters
This commit turns on automated type checking for one more source file (hwilib/psbt.py) and makes small code changes so the file passes the type checker. The actual code changes move two integer assignments slightly later so validation happens before storing the value, and add explicit type annotations to two methods. There is no direct evidence this fixes a security vulnerability; it is primarily a code-quality and type-safety improvement.
No urgent action is required. Treat as a normal code-quality update. If reviewing for security, verify separately that the PSBT deserializer rejects malformed locktime values as intended, since this commit does not materially change that behavior.
Security signals we found
Type checking enabled for PSBT module
Validation ordering tightened for locktime fields
Explicit type annotations added to PSBT methods
Evidence from the diff
The commit adds hwilib/psbt.py to the mypy type-check workflow and adjusts the file accordingly. In PartiallySignedInput.deserialize, the time_locktime and height_locktime values are now unpacked into a local variable, validated, and only then assigned to self. This is behaviorally equivalent to the prior code because the validation immediately follows the unpack and raises before assignment; however, it is cleaner and avoids storing an invalid value if an exception were ever moved. Two method signatures (setup_from_tx and _convert_version) receive explicit return-type and parameter-type annotations. No functional security bug is demonstrably fixed by the diff alone.
Changed components
hwilib/psbt.py.github/workflows/type-check.ymlInspect captured patch +9 / −6
### .github/workflows/type-check.yml
@@ -40,4 +40,5 @@ jobs:
hwilib/hwwclient.py
hwilib/__init__.py
hwilib/key.py
+ hwilib/psbt.py
hwilib/udevinstaller.py
### hwilib/psbt.py
@@ -289,9 +289,10 @@ def deserialize(self, f: Readable) -> None:
v = deser_string(f)
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:
+ time_locktime = struct.unpack("<I", v)[0]
+ if time_locktime < 500000000:
raise PSBTSerializationError("Input time based locktime is less than 500000000")
+ self.time_locktime = time_locktime
elif key_type == PartiallySignedInput.PSBT_IN_REQUIRED_HEIGHT_LOCKTIME:
if self.version == 0:
raise PSBTSerializationError("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME is not allowed in PSBTv0")
@@ -302,9 +303,10 @@ def deserialize(self, f: Readable) -> None:
v = deser_string(f)
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:
+ height_locktime = struct.unpack("<I", v)[0]
+ if height_locktime == 0 or height_locktime >= 500000000:
raise PSBTSerializationError("Input height based locktime is not greater than 0 and less than 500000000")
+ self.height_locktime = height_locktime
elif key_type == PartiallySignedInput.PSBT_IN_TAP_KEY_SIG:
if key in key_lookup:
raise PSBTSerializationError("Duplicate key, input Taproot key signature already provided")
@@ -1072,7 +1074,7 @@ def cache_unsigned_tx_pieces(self) -> None:
if self.version == 0:
self.setup_from_tx(self.tx)
- def setup_from_tx(self, tx: CTransaction):
+ def setup_from_tx(self, tx: CTransaction) -> None:
"""
Fills in the PSBTv2 fields for this PSBT given a transaction
@@ -1159,7 +1161,7 @@ def get_unsigned_tx(self) -> CTransaction:
tx.rehash()
return tx
- def _convert_version(self, version) -> None:
+ def _convert_version(self, version: int) -> None:
self.version = version
for psbt_in in self.inputs:
psbt_in.version = versionWhy this scored 18/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.