Skip to content

Tests: Regression test for multisig change detection and consolidation handling#933

Open
nkatha23 wants to merge 1 commit into
SeedSigner:devfrom
nkatha23:tests/psbt-parser-targeted-cases
Open

Tests: Regression test for multisig change detection and consolidation handling#933
nkatha23 wants to merge 1 commit into
SeedSigner:devfrom
nkatha23:tests/psbt-parser-targeted-cases

Conversation

@nkatha23
Copy link
Copy Markdown

@nkatha23 nkatha23 commented May 7, 2026

In response to issue #931 , this is what I found:

Reading through PSBTParser._parse_outputs(), I noticed two spending scenarios that weren't covered by the existing test suite:

  1. Multisig PSBT with no witness script in the output scope. Some coordinators produce PSBTs where the output scopes contain only the bare scriptPubKey, no witness_script, no redeem_script. The parser can't reconstruct the multisig
    policy from a bare script hash, so it has no way to verify that the output belongs to the wallet. The safe, correct behaviour is to treat it as an external spend. That path existed in the code but was never exercised by a test, meaning a future refactor could silently break it.
  2. Consolidation where every output is internal. A PSBT that sends everything backto the same wallet, split across a change-branch address (1/) and a receive-branch address (0/) should produce spend_amount == 0. The parser
    classifies any output as internal when the derived key matches the output scriptPubKey, regardless of branch index. Again, this worked but wasn't tested.

so this PR adds:
Two new test methods inside TestPSBTParser in tests/test_psbt_parser.py:
test_multisig_no_descriptor_all_outputs_are_spend- covers all three multisig script types. Strips witness_script and redeem_script from the output scope and pins that num_change_outputs == 0 with everything treated as spend.
test_consolidation_all_outputs_internal- covers all seven supported script types. Two internal outputs (change-branch + receive-branch) both belonging to the signing wallet, pins spend_amount == 0 and change_amount == input_amount - fee.

@nkatha23 nkatha23 marked this pull request as draft May 7, 2026 11:22
@nkatha23 nkatha23 force-pushed the tests/psbt-parser-targeted-cases branch from b91a825 to 28c700f Compare May 7, 2026 11:26
@nkatha23 nkatha23 marked this pull request as ready for review May 7, 2026 11:26
@nkatha23 nkatha23 force-pushed the tests/psbt-parser-targeted-cases branch 2 times, most recently from 976f78b to 0e71f30 Compare May 11, 2026 09:45
Comment thread tests/test_psbt_parser.py
Comment thread tests/test_psbt_parser.py
Comment thread tests/test_psbt_parser.py Outdated
Comment thread tests/test_psbt_parser.py Outdated
Comment thread tests/test_psbt_parser.py Outdated
@kdmukai
Copy link
Copy Markdown
Contributor

kdmukai commented May 29, 2026

  1. Multisig PSBT with no witness script in the output scope. Some coordinators produce PSBTs where the output scopes contain only the bare scriptPubKey, no witness_script, no redeem_script.

Can you provide info about the specific coordinator(s) and specific conditions that would create this type of PSBT?

@kdmukai
Copy link
Copy Markdown
Contributor

kdmukai commented May 29, 2026

2. Consolidation where every output is internal. A PSBT that sends everything backto the same wallet, split across a change-branch address (1/) and a receive-branch address (0/) should produce spend_amount == 0. The parser
classifies any output as internal when the derived key matches the output scriptPubKey, regardless of branch index. Again, this worked but wasn't tested.

We already have multisig psbts with self-receive + change as the only outputs. It would be cleaner to just add a few extra checks once those psbts are created:

https://github.com/nkatha23/seedsigner/blob/0e71f300dd1abd587ab74636a3532f3870275a72/tests/test_psbt_parser.py#L268-L283

assert psbt_parser.num_destinations == 0
assert psbt_parser.num_change_outputs == 2
assert psbt_parser.spend_amount == 0
# etc...

@kdmukai
Copy link
Copy Markdown
Contributor

kdmukai commented May 29, 2026

2. Consolidation where every output is internal. A PSBT that sends everything backto the same wallet, split across a change-branch address (1/) and a receive-branch address (0/) should produce spend_amount == 0. The parser
classifies any output as internal when the derived key matches the output scriptPubKey, regardless of branch index.

For single sig, we have a close-enough existing test harness that just needs to be extended to cover this scenario. See run_basic_test.

@kdmukai
Copy link
Copy Markdown
Contributor

kdmukai commented May 29, 2026

As of 0e71f30:

NACK on test_consolidation_all_outputs_internal as existing tests nearly cover it and just need light enhancements to provide the same scenario coverage with less additional harnessing.

No opinion yet on test_multisig_no_descriptor_all_outputs_are_spend until we get more clarification on seeing this in real-world usage.

When a coordinator strips witness_script/redeem_script from multisig output
scopes, _get_policy() returns a bare policy dict with no m/n/cosigners, which
never matches the input policy — so no output is classified as change and all
value shows as spend_amount. This was the correct conservative behaviour but
had no test pinning it.

Also adds a consolidation test: a PSBT where both a change-branch (1/*) and a
receive-branch (0/*) output belong to the signing wallet. PSBTParser classifies
both as internal regardless of branch index, giving spend_amount == 0.

Covers all three multisig types for the no-descriptor case, all seven script
types for the consolidation case. Part of SeedSigner#931.
@nkatha23 nkatha23 force-pushed the tests/psbt-parser-targeted-cases branch from 0e71f30 to ff432aa Compare June 1, 2026 10:41
@nkatha23
Copy link
Copy Markdown
Author

nkatha23 commented Jun 1, 2026

No opinion yet on test_multisig_no_descriptor_all_outputs_are_spend until we get more clarification on seeing this in real-world usage.

No documented coordinator has been shown to produce a multisig PSBT where the change output is missing witness_script. I looked and couldn't find one. The test isn't grounded in a reported user problem, it's a defensive pin of a security property: the parser must never classify an output as change without cryptographic verification of ownership. The risk being guarded against is a future internal refactor, not a current coordinator.

@nkatha23
Copy link
Copy Markdown
Author

nkatha23 commented Jun 1, 2026

  1. Multisig PSBT with no witness script in the output scope. Some coordinators produce PSBTs where the output scopes contain only the bare scriptPubKey, no witness_script, no redeem_script.

Can you provide info about the specific coordinator(s) and specific conditions that would create this type of PSBT?

The test came directly from item 1 in this issue #931 : "Construct a multisig PSBT. Instantiate PSBTParser without loading a descriptor." Since PSBTParser has no descriptor parameter, I interpreted "no descriptor registered" as the output scope lacking witness_script/redeem_script, the fields _get_policy() needs to reconstruct the multisig policy. Without those fields, the output policy dict never matches the input policy, so no output is classified as change.
I couldn't find a specific coordinator documented as producing this shape on change outputs.

@nkatha23 nkatha23 changed the title tests: pin multisig no-descriptor and consolidation behaviour in PSBTParser Tests: Regression test for change output detection in _parse_outputs() Jun 5, 2026
@nkatha23 nkatha23 changed the title Tests: Regression test for change output detection in _parse_outputs() Tests: Regression test for multisig change detection and consolidation handling Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants