Skip to content

Validate sender input UTXO data - #1836

Open
Jolah1 wants to merge 3 commits into
payjoin:masterfrom
Jolah1:validate-sender-input-utxos
Open

Validate sender input UTXO data#1836
Jolah1 wants to merge 3 commits into
payjoin:masterfrom
Jolah1:validate-sender-input-utxos

Conversation

@Jolah1

@Jolah1 Jolah1 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #678.

What

Validate that every original sender outpoint remains present and ordered before classifying additional proposal inputs as receiver-owned. For matched sender inputs, reject returned witness_utxo or non_witness_utxo data when it differs from the original PSBT while continuing to accept fields stripped according to BIP 78.

Add a dedicated proposal error for modified sender UTXO information and regression tests for altered outpoints, witness UTXOs, and non-witness UTXOs. Existing receiver-input tests now exercise the proposal's actual receiver input.

Why

Previously, an altered sender outpoint was misclassified as a receiver input and could produce a misleading finalization error. Modified sender UTXO metadata was silently overwritten during restoration instead of reporting receiver misbehavior.

Tests

  • cargo test -p payjoin test_sender_input_ --all-features
  • cargo test -p payjoin test_receiver_input_ --all-features
  • cargo test -p payjoin --all-features
  • cargo fmt --all -- --check
  • cargo clippy -p payjoin --all-targets --all-features -- -D warnings
  • codespell

Disclosure: co-authored by Codex

@Jolah1
Jolah1 marked this pull request as ready for review August 24, 2026 13:40
@coveralls

coveralls commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 33793175073

Coverage increased (+0.07%) to 86.706%

Details

  • Coverage increased (+0.07%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (107 of 108 lines covered, 99.07%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
payjoin/src/core/send/error.rs 2 1 50.0%
Total (2 files) 108 107 99.07%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 16564
Covered Lines: 14362
Line Coverage: 86.71%
Coverage Strength: 341.49 hits per line

💛 - Coveralls

@caarloshenriq caarloshenriq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK d576e05

The security logic is sound. The outpoint pre-check correctly uses a single advancing iterator with any() to enforce that all original outpoints appear as an ordered subsequence in the proposal, catching modified outpoints early instead of misclassifying them as receiver inputs. The UTXO validation with is_none_or correctly accepts stripped fields (per BIP 78) while rejecting modified or injected data. The fix to the existing receiver-input tests (operating on input[1] instead of mangling input[0]) is a genuine improvement: those tests now exercise the actual receiver input.

Two commit hygiene points:

This commit does three things: outpoint pre-check, UTXO data validation, and existing test fixes. Per project conventions each logical unit should be its own commit. At minimum the test fixes should be separate, since they're independently correct and useful regardless of the new validation.

The commit message has no body. For a security-relevant change to PSBT validation, it would be worth explaining the threat model (receiver modifying UTXO data to invalidate signatures or cause the sender to sign a different transaction). The context in #678 covers this well and the body could reference it.

These tests mangled the first input's outpoint so that the sender's own
input would be classified as receiver-contributed, then asserted the
receiver checks against it. The proposal already carries a real receiver
input at index 1, so the setup was both indirect and never exercised the
input it claimed to cover.

Point the assertions at input 1 and drop the outpoint mangling. The
checks under test are unchanged; only the input they run against is.
@Jolah1
Jolah1 force-pushed the validate-sender-input-utxos branch from d576e05 to bd2186a Compare August 25, 2026 03:50
@Jolah1

Jolah1 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Concept ACK d576e05

The security logic is sound. The outpoint pre-check correctly uses a single advancing iterator with any() to enforce that all original outpoints appear as an ordered subsequence in the proposal, catching modified outpoints early instead of misclassifying them as receiver inputs. The UTXO validation with is_none_or correctly accepts stripped fields (per BIP 78) while rejecting modified or injected data. The fix to the existing receiver-input tests (operating on input[1] instead of mangling input[0]) is a genuine improvement: those tests now exercise the actual receiver input.

Two commit hygiene points:

This commit does three things: outpoint pre-check, UTXO data validation, and existing test fixes. Per project conventions each logical unit should be its own commit. At minimum the test fixes should be separate, since they're independently correct and useful regardless of the new validation.

The commit message has no body. For a security-relevant change to PSBT validation, it would be worth explaining the threat model (receiver modifying UTXO data to invalidate signatures or cause the sender to sign a different transaction). The context in #678 covers this well and the body could reference it.

Thanks — split into three commits: the test fixes first (they pass on master's validation logic unchanged, so they stand alone as you said), then the outpoint pre-check, then the UTXO data validation with its error variant.
Each builds and passes cargo test --all-features + clippy independently; verified the three together reproduce the original tree exactly. Commit bodies now cover the threat model for each change and reference #678. Also
rebased onto current master while I was rewriting.

@caarloshenriq caarloshenriq left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK bd2186a

All sender-input validation tests (test_sender_input_outpoint_changed, test_sender_input_witness_utxo_changed, test_sender_input_non_witness_utxo_changed) and the corrected receiver-input tests pass.

xstoicunicornx
xstoicunicornx previously approved these changes Sep 2, 2026

@xstoicunicornx xstoicunicornx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK bd2186a.

PR addresses some deficiencies in accurate error reporting when the sender is validating the payjoin proposal. None of these changes are absolutely necessary but do improve the clarity of our tests and error reporting.

Provided some small feedback.

Comment thread payjoin/src/core/send/mod.rs Outdated
Comment thread payjoin/src/core/send/mod.rs
Input classification treats any proposed input whose outpoint does not
match an original one as receiver-contributed. A receiver that alters an
original outpoint therefore escapes the sender-side checks entirely: the
input is validated as though the receiver had contributed it, and the
sender only learns something is wrong later, from a finalization error
that points at the wrong thing.

Check up front that every original outpoint is still present in the
proposal, in its original relative order, before classifying any input.
is_ordered_subsequence enforces that with a single advancing iterator,
so dropped and reordered inputs are both rejected as
MissingOrShuffledInputs. Keeping it a free function lets the ordering
rule be tested directly, without a proposal vector carrying several
sender inputs.

See payjoin#678.
@Jolah1

Jolah1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

utACK bd2186a.

PR addresses some deficiencies in accurate error reporting when the sender is validating the payjoin proposal. None of these changes are absolutely necessary but do improve the clarity of our tests and error reporting.

Provided some small feedback.

updated bd2186a6c62fb2: extracted is_ordered_subsequence with a unit test, split the UTXO check into two ensures, and added a test for unchanged/stripped data. Both folded into their existing commits, so the three-commit structure is unchanged. Re-ACK when you have a moment.

Comment thread payjoin/src/core/send/mod.rs Outdated
InternalProposalError::SenderTxinNonAllSighashType,
)?;
}
// BIP 78 lets the receiver strip these fields, but never alter them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BIP 78 doesn't currently say anything about these fields, we are enforcing this arbitrarily. Which I think is fine, but thinking about it more would like others to weigh in too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this is policy, not spec, and the comment now says so. It also doesn't close a hole: restore_original_utxos overwrites these fields two lines after the check, so the sender signs its own values either way. The gain is a named error instead of a silent overwrite. Happy to drop this commit if the preference is status quo.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... yes could we please drop this commit for now? Feel free to open as an issue to follow up on this question of whether we should enforce this policy even if not in the specification. Careful, I think some updates for the Reject proposals missing original sender inputs commit got entangled in this commit.

Comment thread payjoin/src/core/send/mod.rs
Comment thread payjoin/src/core/send/mod.rs
Comment thread payjoin/src/core/send/mod.rs Outdated
The sender restores UTXO information onto its own inputs after the
receiver returns a proposal, because many wallets need it to sign.
Restoration overwrote whatever came back unconditionally, so a
receiver could return altered witness_utxo or non_witness_utxo data
and have it silently replaced rather than reported. The sender lost
the one signal that the receiver had tampered with its inputs.

Compare the returned UTXO data against the original PSBT before
restoring it, and reject any mismatch as SenderTxinUtxoInfoChanged.
Absent fields are still accepted, tolerating receivers that strip
data they do not need, and the two field checks stay separate so
each reads on its own.

BIP 78 says nothing about altering these fields: its reference
implementation overwrites them without reading them, and its
proposal vectors return them unchanged. Rejecting a mismatch is a
policy choice, not a spec requirement.

See payjoin#678.
@Jolah1
Jolah1 force-pushed the validate-sender-input-utxos branch from 6c62fb2 to 6db2b1d Compare September 3, 2026 18:53
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.

Previous UTXO validation for sender

4 participants