diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py index a3c1ab913..2cf448f48 100644 --- a/tests/test_psbt_parser.py +++ b/tests/test_psbt_parser.py @@ -284,6 +284,104 @@ def test_verify_multisig_output(self): + def test_multisig_no_descriptor_all_outputs_are_spend(self): + cases = [ + (PSBTTestData.MULTISIG_NATIVE_SEGWIT_1_INPUT, PSBTTestData.MULTISIG_NATIVE_SEGWIT_CHANGE), + (PSBTTestData.MULTISIG_NESTED_SEGWIT_1_INPUT, PSBTTestData.MULTISIG_NESTED_SEGWIT_CHANGE), + (PSBTTestData.MULTISIG_LEGACY_P2SH_1_INPUT, PSBTTestData.MULTISIG_LEGACY_P2SH_CHANGE), + ] + + fee_amount = 5_000 + for psbt_base64, change_data in cases: + psbt: PSBT = PSBT.parse(a2b_base64(psbt_base64)) + input_amount = sum(inp.utxo.value for inp in psbt.inputs) + output_amount = input_amount - fee_amount + + stripped = create_output(change_data, output_amount) + stripped.witness_script = None + stripped.redeem_script = None + psbt.outputs.append(stripped) + + psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST) + + assert psbt_parser.num_change_outputs == 0, \ + "Without witness_script/redeem_script the output policy cannot match; no change should be detected" + assert psbt_parser.num_destinations == 1, \ + "The unverifiable output must fall through to destination_addresses" + assert psbt_parser.spend_amount == output_amount, \ + "The full output value is treated as a spend when the multisig script is absent" + assert psbt_parser.change_amount == 0, \ + "No output can be verified as change without the multisig script" + assert psbt_parser.fee_amount == fee_amount + assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount + + + def test_consolidation_all_outputs_internal(self): + cases = [ + ( + PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_1_INPUT, + PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_CHANGE, + PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_SELF_TRANSFER, + ), + ( + PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_1_INPUT, + PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_CHANGE, + PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_SELF_TRANSFER, + ), + ( + PSBTTestData.SINGLE_SIG_TAPROOT_1_INPUT, + PSBTTestData.SINGLE_SIG_TAPROOT_CHANGE, + PSBTTestData.SINGLE_SIG_TAPROOT_SELF_TRANSFER, + ), + ( + PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_1_INPUT, + PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_CHANGE, + PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_SELF_TRANSFER, + ), + ( + PSBTTestData.MULTISIG_NATIVE_SEGWIT_1_INPUT, + PSBTTestData.MULTISIG_NATIVE_SEGWIT_CHANGE, + PSBTTestData.MULTISIG_NATIVE_SEGWIT_SELF_TRANSFER, + ), + ( + PSBTTestData.MULTISIG_NESTED_SEGWIT_1_INPUT, + PSBTTestData.MULTISIG_NESTED_SEGWIT_CHANGE, + PSBTTestData.MULTISIG_NESTED_SEGWIT_SELF_TRANSFER, + ), + ( + PSBTTestData.MULTISIG_LEGACY_P2SH_1_INPUT, + PSBTTestData.MULTISIG_LEGACY_P2SH_CHANGE, + PSBTTestData.MULTISIG_LEGACY_P2SH_SELF_TRANSFER, + ), + ] + + fee_amount = 5_000 + for psbt_base64, change_data, self_transfer_data in cases: + psbt: PSBT = PSBT.parse(a2b_base64(psbt_base64)) + input_amount = sum(inp.utxo.value for inp in psbt.inputs) + total_output = input_amount - fee_amount + # Exact split does not matter; both outputs must be recognised as internal + change_output_amount = total_output // 2 + self_transfer_amount = total_output - change_output_amount + + psbt.outputs.append(create_output(change_data, change_output_amount)) + psbt.outputs.append(create_output(self_transfer_data, self_transfer_amount)) + + psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST) + + assert psbt_parser.num_destinations == 0, \ + "All outputs belong to the signing wallet; no external recipients expected" + assert psbt_parser.num_change_outputs == 2, \ + "Both change-branch (1/*) and receive-branch (0/*) outputs must be classified as internal" + assert psbt_parser.spend_amount == 0, \ + "No value is sent to an external address in a full consolidation" + assert psbt_parser.change_amount == total_output, \ + "All output value (minus fee) must be retained internally" + assert psbt_parser.fee_amount == fee_amount + assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount + + + # TODO: Refactor all tests to be in the TestPSBTParser class(?) def test_p2tr_change_detection(): """ Should successfully detect change in a p2tr to p2tr psbt spend