Reject invalid PSBT output amounts
What changed, and why it matters
This commit adds a safety check to the Passport hardware wallet's Bitcoin transaction parser. Before signing a transaction, the device now rejects any output amount that is negative, larger than the total possible Bitcoin supply (about 21 million BTC), or that would make the whole transaction output more than that total. Without this check, a malicious or malformed transaction could try to use impossible amounts, which might confuse the user or lead the wallet to compute incorrect balances or fees.
Review whether similar validation is needed elsewhere that parses transaction outputs or computes totals, and confirm the new unit tests are run in CI. Consider whether MAX_MONEY should also be enforced for input values and fee calculations.
Security signals we found
Adds input validation for transaction output amounts
Introduces consensus-level monetary bound check
Prevents negative or overflow-like output values from being processed
Raises FatalPSBTIssue to abort PSBT processing on invalid amounts
Adds unit tests covering boundary and edge cases
Evidence from the diff
The patch introduces a MAX_MONEY constant (21,000,000,000,000,000 satoshis) and validates each CTxOut.nValue during psbtObject.output_iter(). It raises FatalPSBTIssue if an individual output is outside [0, MAX_MONEY] or if the running total exceeds MAX_MONEY. A unit test file exercises boundary values: 0, MAX_MONEY, MAX_MONEY split across two outputs, and invalid cases -1, MAX_MONEY+1, and MAX_MONEY+1 total.
Changed components
ports/stm32/boards/Passport/modules/psbt.pyports/stm32/boards/Passport/modules/public_constants.pyports/stm32/boards/Passport/modules/tests/test_unit.pyports/stm32/boards/Passport/modules/tests/unit/psbt_output_values.pyInspect captured patch +68 / −1
### ports/stm32/boards/Passport/modules/psbt.py
@@ -17,7 +17,7 @@
import sys
from sffile import SizerFile
from passport import mem
-from public_constants import MAX_SIGNERS
+from public_constants import MAX_MONEY, MAX_SIGNERS
from multisig_wallet import MultisigWallet, disassemble_multisig_mn
from exceptions import FatalPSBTIssue, FraudulentChangeOutput
from serializations import ser_compact_size, deser_compact_size, hash160, deser_compact_size_bytes
@@ -1004,7 +1004,12 @@ def output_iter(self):
tx_out.deserialize(fd)
+ if not 0 <= tx_out.nValue <= MAX_MONEY:
+ raise FatalPSBTIssue('Invalid amount for output #%d' % idx)
+
total_out += tx_out.nValue
+ if total_out > MAX_MONEY:
+ raise FatalPSBTIssue('Total output amount exceeds maximum')
cont = fd.tell()
yield idx, tx_out
### ports/stm32/boards/Passport/modules/public_constants.py
@@ -13,6 +13,9 @@
from constants import PSBT_MAX_SIZE
+# Maximum number of satoshis permitted by Bitcoin consensus.
+MAX_MONEY = const(2100000000000000)
+
# Seed phrase lengths accepted
SEED_LENGTHS = [12, 24]
SEED_WORD_LIST_LENGTH = const(2048)
### ports/stm32/boards/Passport/modules/tests/test_unit.py
@@ -26,3 +26,7 @@ def test_ui(test):
def test_foundation(test):
assert test('foundation.py') == b'OK'
+
+
+def test_psbt_output_values(test):
+ assert test('psbt_output_values.py') == b'OK'
### ports/stm32/boards/Passport/modules/tests/unit/psbt_output_values.py
@@ -0,0 +1,55 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# Test consensus bounds for output amounts in unsigned PSBT transactions.
+
+from uio import BytesIO
+
+from exceptions import FatalPSBTIssue
+from psbt import psbtObject
+from public_constants import MAX_MONEY
+from serializations import CTxOut
+
+
+P2WPKH_SCRIPT = b'\x00\x14' + (b'\x11' * 20)
+
+
+class FakePSBT:
+ def __init__(self, values):
+ self.fd = BytesIO(b''.join(CTxOut(value, P2WPKH_SCRIPT).serialize() for value in values))
+ self.vout_start = 0
+ self.num_outputs = len(values)
+ self.total_value_out = None
+
+
+def read_outputs(values):
+ psbt = FakePSBT(values)
+ parsed = [tx_out.nValue for _, tx_out in psbtObject.output_iter(psbt)]
+ return psbt, parsed
+
+
+def assert_invalid(values):
+ try:
+ read_outputs(values)
+ except FatalPSBTIssue:
+ return
+ raise AssertionError('Expected invalid output values to be rejected')
+
+
+zero, parsed = read_outputs([0])
+assert parsed == [0]
+assert zero.total_value_out == 0
+
+maximum, parsed = read_outputs([MAX_MONEY])
+assert parsed == [MAX_MONEY]
+assert maximum.total_value_out == MAX_MONEY
+
+split_maximum, parsed = read_outputs([MAX_MONEY - 1, 1])
+assert parsed == [MAX_MONEY - 1, 1]
+assert split_maximum.total_value_out == MAX_MONEY
+
+assert_invalid([-1])
+assert_invalid([MAX_MONEY + 1])
+assert_invalid([MAX_MONEY, 1])
+
+return_value.write(b'OK')Why this scored 59/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.