Skip to content

Fix try_preserving_privacy returning Empty when candidates exist#1629

Open
DanGould wants to merge 1 commit into
payjoin:masterfrom
DanGould:fix-uih-avoidance
Open

Fix try_preserving_privacy returning Empty when candidates exist#1629
DanGould wants to merge 1 commit into
payjoin:masterfrom
DanGould:fix-uih-avoidance

Conversation

@DanGould

@DanGould DanGould commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@Talej tipped me off that try_preserving_privacy was returning Err(Empty) when avoid_uih failed. I took a closer look. Because try_preserving_privacy takes an owned candidate_inputs: impl IntoIterator<Item = InputPair> the IntoIterator is there for ergonomics but the candidate_inputsgets exhausted by the iteration that happens withavoid_uih. Then select_fallback_candidate` selects from an empty iterator. Doh!

This also randomly selects a coin in the case avoid_uih from the candidates fails in an attempt to reduce fingerprints that may be caused by a wallet ordering outputs a certain way, as the second commit. End users can always override this with manual selection. If reviewers think I'm conflating two independent decisions I am happy to separate the commits into their own PRs.

random_fallback_candidate (or its ancestor select_fallback_candidate) both consumed the iterator, so we really gotta collect no matter what. This is a convenience method. It's still possible for more complex / large utxo wallets to implement more performance-conscioius custom coin selection if they need to.

I think this change does need to get into 1.0 (but since the api doesn't change I suppose it could be a patch).

Disclosure: Cursor wrote the code, I reviewed.

Pull Request Checklist

Please confirm the following before requesting review:

Collect candidate inputs before coin selection so the fallback
still sees the full list when avoid_uih scans every candidate and
returns NotFound. Without this the fallback sees an empty list.
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 27207089758

Coverage increased (+0.04%) to 85.429%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 48 of 48 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 14721
Covered Lines: 12576
Line Coverage: 85.43%
Coverage Strength: 370.69 hits per line

💛 - Coveralls

@DanGould DanGould requested a review from chavic June 9, 2026 12:57
@DanGould DanGould marked this pull request as ready for review June 9, 2026 13:23
@nothingmuch

Copy link
Copy Markdown
Contributor

This also randomly selects a coin in the case avoid_uih from the candidates fails in an attempt to reduce fingerprints that may be caused by a wallet ordering outputs a certain way, as the second commit. End users can always override this with manual selection. If reviewers think I'm conflating two independent decisions I am happy to separate the commits into their own PRs.

while the baseline behavior seems far from optimal, this change potentially increases the rate at which cluster information is leaked in a way that is possibly compounded by utxo probing relative when the sender is adversarial

assuming the input order is stable, picking consistently can also reduce the overall privacy leaks and is in line with BIP 78 and 79 anti probing

so this seems to make fingerprinting potentially better but not clearly so and with unclear impact, but probing worse

privacy impacting changes like this should have a rationale and analysis to back it up

@arminsabouri

Copy link
Copy Markdown
Contributor

If reviewers think I'm conflating two independent decisions I am happy to separate the commits into their own PRs.

Yes please seperate these. One is a bug fix the other is a privacy improvment that should be scrutinized more.
Do we have data to back this up? i.e how are integrations collecting inputs? @bc1cindy is already doing the hardwork of collecting fingerprints from our current integrations here: #1597

@bc1cindy bc1cindy 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.

the bug fix is correct and worth keeping, my only concern is the random fallback

Comment thread payjoin/src/core/receive/common/mod.rs Outdated
/// Privacy preservation is only supported for 2-output transactions. If the PSBT has more than
/// 2 outputs or if none of the candidates are suitable for avoiding UIH2, this function
/// defaults to the first candidate in `candidate_inputs` list.
/// randomly selects one of the provided candidates.

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.

randomizing the contributed-input selection conflicts with BIP 79's anti-probing requirement

"it is essential that the receiver always returns the same contributed inputs when it's seen the same inputs."

to prevent UTXO-set enumeration.

the deterministic collection in 5/6 integrations (#1597 (comment)) approximates this, a random fallback breaks it.

bc1cindy added a commit to bc1cindy/rust-payjoin that referenced this pull request Jun 10, 2026
Pin the non-empty-candidates invariant the payjoin#1629 fix restores.
bc1cindy added a commit to bc1cindy/rust-payjoin that referenced this pull request Jun 10, 2026
Pin the non-empty-candidates invariant the payjoin#1629 fix restores.

@bc1cindy bc1cindy 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.

I wrote a proptest that catches this bug, which fails without this fix and passes with it: bc1cindy/rust-payjoin@362a3fb4

makes sense to add the dep?

@DanGould DanGould force-pushed the fix-uih-avoidance branch from 6af29d3 to efc5b92 Compare June 10, 2026 14:55
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.

5 participants