What changed, and why it matters
This commit fixes a bug in Krux, a Bitcoin signing device, where a malformed transaction whose outputs spend more than its inputs could be loaded and shown to the user. Normally such a transaction is impossible on the Bitcoin network, but Krux would display it with a tiny negative fee and skip the high-fee warning, potentially tricking a user into approving a transaction that can never be mined. The fix now rejects these PSBTs immediately when loading them.
Review whether other amount-related edge cases (e.g., integer overflow, zero inputs, duplicate inputs) are similarly validated before UI display. Ensure the error message is surfaced clearly to the user.
Security signals we found
Input validation gap in PSBT parsing
UI rendering bug masking invalid transaction economics
Potential social-engineering / user-confusion attack
Missing sanity check on input/output amounts
Evidence from the diff
The patch adds a validation check in PSBTSigner construction: if the sum of output values exceeds the sum of input UTXO values, it raises ValueError(‘outputs exceed inputs’). Previously the fee calculation produced a negative number, which the UI rendered as a small negative fee and clamped fee_percent to 0.1, bypassing the high-fee warning. The commit also adds unit tests covering both rejection of outputs-exceed-inputs and acceptance of a zero-fee transaction.
Changed components
src/krux/psbt.pyPSBTSigner classInspect captured patch +52 / −0
diff --git a/src/krux/psbt.py b/src/krux/psbt.py
index 6dadeff..bdf892e 100644
--- a/src/krux/psbt.py
+++ b/src/krux/psbt.py
@@ -178,6 +178,13 @@ class PSBTSigner:
if self.policy != inp_policy:
raise ValueError("mixed inputs in the tx")
+ # A transaction spending more than it funds is invalid and would render
+ # as a negative fee, which reads like a cheap transaction on screen
+ if sum(out.value for out in self.psbt.outputs) > sum(
+ inp.utxo.value for inp in self.psbt.inputs
+ ):
+ raise ValueError("outputs exceed inputs")
+
if self.wallet.is_miniscript():
if not is_miniscript(self.policy):
raise ValueError("Not a miniscript PSBT")
diff --git a/tests/test_psbt_input_amounts.py b/tests/test_psbt_input_amounts.py
index 0c8a9b6..28cf828 100644
--- a/tests/test_psbt_input_amounts.py
+++ b/tests/test_psbt_input_amounts.py
@@ -144,6 +144,51 @@ def test_rejects_legacy_input_without_previous_tx(m5stickv):
PSBTSigner(_wallet(), psbt.serialize(), FORMAT_NONE)
+def _segwit_psbt(root, input_value, output_value):
+ """Single input p2wpkh PSBT with the given declared amounts"""
+ from embit import script
+ from embit.psbt import PSBT
+ from embit.transaction import Transaction, TransactionInput, TransactionOutput
+
+ pubkey, derivation = _key_at(root, "m/84h/1h/0h/0/0")
+ tx = Transaction(
+ vin=[TransactionInput(b"\x99" * 32, 0)],
+ vout=[
+ TransactionOutput(
+ output_value, script.p2wpkh(_key_at(root, "m/84h/1h/0h/0/7")[0])
+ )
+ ],
+ )
+ psbt = PSBT(tx)
+ psbt.inputs[0].witness_utxo = TransactionOutput(input_value, script.p2wpkh(pubkey))
+ psbt.inputs[0].bip32_derivations[pubkey] = derivation
+ return psbt.serialize()
+
+
+def test_rejects_outputs_exceeding_inputs(m5stickv):
+ """A negative fee is impossible on chain and must not reach the review screen.
+
+ It would render as a small negative amount and fee_percent clamps to 0.1,
+ so the high fee warning would not fire either.
+ """
+ from krux.psbt import PSBTSigner
+ from krux.qr import FORMAT_NONE
+
+ root = _root()
+ with pytest.raises(ValueError, match="outputs exceed inputs"):
+ PSBTSigner(_wallet(), _segwit_psbt(root, 99000, 100000), FORMAT_NONE)
+
+
+def test_accepts_zero_fee(m5stickv):
+ """A zero fee is unusual but valid, only a negative one is impossible"""
+ from krux.psbt import PSBTSigner
+ from krux.qr import FORMAT_NONE
+
+ root = _root()
+ signer = PSBTSigner(_wallet(), _segwit_psbt(root, 100000, 100000), FORMAT_NONE)
+ assert isinstance(signer, PSBTSigner)
+
+
def _compressed_psbt_with_contradicting_amounts(root, real_value, declared_value):
"""Builds a PSBT that forces the compressed parse and lies in witness_utxo.
Why this scored 66/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.