Tests for showing transactions with external inputs
What changed, and why it matters
This commit only adds and updates automated test code for the Ledger Bitcoin app. It introduces new test cases that verify how the device screen displays transactions containing external inputs and unusual signature rules. There are no changes to the actual app firmware, wallet logic, or security-critical code.
No security action required; this is a test-only change. Routine review/CI pass is sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies three test files under tests/. It updates the Ragger navigator instructions in tests/instructions.py so that warning prompts for non-standard sighash, external inputs, and unverified warnings are handled within the same APDU request group. It adds test_sign_psbt_with_external_inputs_net_receive in tests/test_sign_psbt.py and two new tests (test_sighash_single_external_inputs_net_only, test_sighash_all_anyonecanpay_external_inputs_net_only) in tests/test_sign_psbt_with_sighash_types.py. These tests exercise PSBT signing flows with external inputs and net-receive scenarios, asserting that the expected number of hardware-wallet signatures is produced.
Changed components
tests/instructions.pytests/test_sign_psbt.pytests/test_sign_psbt_with_sighash_types.pyInspect captured patch +63 / −3
diff --git a/tests/instructions.py b/tests/instructions.py
index e8d167e..0c9a236 100644
--- a/tests/instructions.py
+++ b/tests/instructions.py
@@ -225,19 +225,24 @@ def sign_psbt_instruction_approve(model: Firmware, save_screenshot: bool = True,
run_num = run_num + 1
else:
+ # All the warnings below, plus the review that follows, are shown by the firmware
+ # within a single APDU exchange. They must therefore live in one request group
if has_sighashwarning:
# This transaction uses non-standard signing rules- actually clicking "Continue anyway"
- instructions.choice_reject("Continue anyway")
+ funcdict[which_func]("Continue anyway", NavInsID.USE_CASE_REVIEW_TAP,
+ NavInsID.USE_CASE_CHOICE_REJECT, save_screenshot=save_screenshot)
which_func = 'same_request'
if has_external_inputs:
# This transaction has external inputs- actually clicking "Continue anyway"
- instructions.choice_reject("Continue anyway")
+ funcdict[which_func]("Continue anyway", NavInsID.USE_CASE_REVIEW_TAP,
+ NavInsID.USE_CASE_CHOICE_REJECT, save_screenshot=save_screenshot)
which_func = 'same_request'
if has_unverifiedwarning:
# Non-default sighash - actually clicking "Continue anyway"
- instructions.choice_reject("Continue anyway")
+ funcdict[which_func]("Continue anyway", NavInsID.USE_CASE_REVIEW_TAP,
+ NavInsID.USE_CASE_CHOICE_REJECT, save_screenshot=save_screenshot)
which_func = 'same_request'
funcdict[which_func]("Review", NavInsID.USE_CASE_REVIEW_TAP, NavInsID.USE_CASE_REVIEW_TAP,
diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py
index d887b5a..7a8af0a 100644
--- a/tests/test_sign_psbt.py
+++ b/tests/test_sign_psbt.py
@@ -895,6 +895,30 @@ def test_sign_psbt_with_external_inputs(navigator: Navigator, firmware: Firmware
assert len(hww_sigs) == 1
+def test_sign_psbt_with_external_inputs_net_receive(navigator: Navigator, firmware: Firmware, client:
+ RaggerClient, test_name: str):
+ # Three inputs and two outputs. Only the Taproot input belongs to this wallet; the change
+ # output is larger than that internal input, making the transaction a net receive. The review
+ # shows the external-input warning, external-input total, and net amount received.
+ psbt_b64 = "cHNidP8BAM8CAAAAA6G4I9IzbWlLSTTvm25bfeF6BVE9qKKdsCouy8eppv5tAQAAAAD9////USLCzeaCPlV1QXW5LJxXoKjhrIPDjheH/Tof8zSOlRMBAAAAAP3///96Kpl5VsCfjqf9KBnBqYe7FOIr+a3NryCol2NyLO7iZAEAAAAA/f///wKghgEAAAAAABYAFBOZuKCYR6A5sDUvWNISwYC6sX93ATboAAAAAAAiUSDY9PHRiZbbYbscr1Qj3iXYoe7pyVDTfm/6AjAS1eYuDQAAAAAAAQErp4apAAAAAAAiUSDY9PHRiZbbYbscr1Qj3iXYoe7pyVDTfm/6AjAS1eYuDSEWIS6ihWpc8RDmaivp1zUxR5P9vLOIsrjxPytq3pguevUZAPWswv1WAACAAQAAgAAAAIABAAAAAAAAAAEXICEuooVqXPEQ5mor6dc1MUeT/byziLK48T8rat6YLnr1AAEAjAIAAAAB7CMOUwlSVgUqJCgnDuwEmJRLEPbxxXj0McI9AJi0rloBAAAAFxYAFCgVOYIOLelzrkG6YAS0MckhxNht/v///wJycnUAAAAAABepFMi5Bq8pjHDmA6KMPvwvrhnmqygPh0BCDwAAAAAAGXapFMuuW1DPk55vUxuKa3q9eI/hSwKXiKyE8hwAIgYC7oYIIH4hAoQm9p52RH1+PV4HcEn15oPDE2wjFHYqRxgY9azC/SwAAIABAACAAAAAgAAAAAAAAAAAAAEAfQIAAAABr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBAAAAAP3///8CcBEBAAAAAAAiACD97kQcVuWwbQ34LBJMcHAUpcoZZYvguYVryhbx7TJZ96X0MAAAAAAAFgAUOvhCmtWVSqXuijPJg/2KHoZ5kksAAAAAAQEfpfQwAAAAAAAWABQ6+EKa1ZVKpe6KM8mD/YoehnmSSyIGA+4sPZjrH5PAoaqOWkAJtw63tE6tFfFmbxNrASrVjTBoGPWswv1UAACAAQAAgAAAAIABAAAACAAAAAAAAQUgIS6ihWpc8RDmaivp1zUxR5P9vLOIsrjxPytq3pguevUhByEuooVqXPEQ5mor6dc1MUeT/byziLK48T8rat6YLnr1GQD1rML9VgAAgAEAAIAAAACAAQAAAAAAAAAA"
+ psbt = PSBT()
+ psbt.deserialize(psbt_b64)
+
+ wallet = WalletPolicy(
+ "",
+ "tr(@0/**)",
+ [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U"
+ ],
+ )
+
+ hww_sigs = client.sign_psbt(psbt, wallet, None, navigator,
+ instructions=sign_psbt_instruction_approve(firmware, has_external_inputs=True),
+ testname=test_name)
+
+ assert len(hww_sigs) == 1
+
+
def test_sign_psbt_miniscript_multikey(navigator: Navigator, firmware: Firmware, client:
RaggerClient, test_name: str):
# An earlier (unreleased) version of the app had issues in recognizing the internal key in
diff --git a/tests/test_sign_psbt_with_sighash_types.py b/tests/test_sign_psbt_with_sighash_types.py
index ff9c709..a247c9e 100644
--- a/tests/test_sign_psbt_with_sighash_types.py
+++ b/tests/test_sign_psbt_with_sighash_types.py
@@ -768,6 +768,37 @@ def test_sighash_two_outputs_anyonecanpay_net_only(navigator: Navigator, firmwar
assert result[0][1].signature[-1] == 0x81
+def test_sighash_single_external_inputs_net_only(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str):
+ # NET_ONLY (SIGHASH_SINGLE, one committed output) with an external input and a closed input set:
+ # the "External inputs amount" row is trustworthy here too, so it is shown alongside the net
+ # "You spend" and the untrusted "Fees: Not available".
+ toggle_nonstandard_sighash_setting(navigator, firmware)
+ # Two-input P2WPKH PSBT: input 0 is ours with SIGHASH_SINGLE, input 1 is an external
+ # 500,000-sat input, and the single output includes those external funds minus a 145-sat fee.
+ psbt = PSBT()
+ psbt.deserialize("cHNidP8BAH4CAAAAAnoqmXlWwJ+Op/0oGcGph7sU4iv5rc2vIKiXY3Is7uJkAQAAAAD9////EREREREREREREREREREREREREREREREREREREREREREAAAAAAP////8BNJU4AAAAAAAZdqkUNEoPSMoVDsK5A4F2YLm2ixOmcCaIrAAAAAAAAQB9AgAAAAGvv64GWQ90H/GvWbasRhEmM2pMSoLbVT32/vq3N6wz8wEAAAAA/f///wJwEQEAAAAAACIAIP3uRBxW5bBtDfgsEkxwcBSlyhlli+C5hWvKFvHtMln3pfQwAAAAAAAWABQ6+EKa1ZVKpe6KM8mD/YoehnmSSwAAAAABAR+l9DAAAAAAABYAFDr4QprVlUql7oozyYP9ih6GeZJLAQMEAwAAACIGA+4sPZjrH5PAoaqOWkAJtw63tE6tFfFmbxNrASrVjTBoGPWswv1UAACAAQAAgAAAAIABAAAACAAAAAABAR8goQcAAAAAABYAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
+ result = client.sign_psbt(psbt, wpkh_wallet, None, navigator,
+ instructions=sign_psbt_instruction_approve(firmware, has_sighashwarning=True, has_external_inputs=True),
+ testname=test_name)
+ assert len(result) == 1
+ assert result[0][1].signature[-1] == 0x03
+
+
+def test_sighash_all_anyonecanpay_external_inputs_net_only(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str):
+ # NET_ONLY (ANYONECANPAY | ALL) with an external input and an open input set: the output and
+ # net "You spend" are shown, but the mutable "External inputs amount" must be omitted.
+ toggle_nonstandard_sighash_setting(navigator, firmware)
+ # Same two-input P2WPKH structure as above, but input 0 uses ANYONECANPAY | ALL, leaving the
+ # input set open even though the single output is committed.
+ psbt = PSBT()
+ psbt.deserialize("cHNidP8BAH4CAAAAAnoqmXlWwJ+Op/0oGcGph7sU4iv5rc2vIKiXY3Is7uJkAQAAAAD9////EREREREREREREREREREREREREREREREREREREREREREAAAAAAP////8BNJU4AAAAAAAZdqkUNEoPSMoVDsK5A4F2YLm2ixOmcCaIrAAAAAAAAQB9AgAAAAGvv64GWQ90H/GvWbasRhEmM2pMSoLbVT32/vq3N6wz8wEAAAAA/f///wJwEQEAAAAAACIAIP3uRBxW5bBtDfgsEkxwcBSlyhlli+C5hWvKFvHtMln3pfQwAAAAAAAWABQ6+EKa1ZVKpe6KM8mD/YoehnmSSwAAAAABAR+l9DAAAAAAABYAFDr4QprVlUql7oozyYP9ih6GeZJLAQMEgQAAACIGA+4sPZjrH5PAoaqOWkAJtw63tE6tFfFmbxNrASrVjTBoGPWswv1UAACAAQAAgAAAAIABAAAACAAAAAABAR8goQcAAAAAABYAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
+ result = client.sign_psbt(psbt, wpkh_wallet, None, navigator,
+ instructions=sign_psbt_instruction_approve(firmware, has_sighashwarning=True, has_external_inputs=True),
+ testname=test_name)
+ assert len(result) == 1
+ assert result[0][1].signature[-1] == 0x81
+
+
def test_sighash_mixed_across_inputs_unavailable(navigator: Navigator, firmware: Firmware, client: RaggerClient, test_name: str):
# Two internal inputs signed with different (non-default) sighashes: the signed inputs
# disagree on what is committed, so no coherent amount/fee can be shown -> UNAVAILABLE.
Why this scored 15/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.