Skip to content

Randomize receiver input selection and consolidate proposal signature stripping - #7

Closed
chavic wants to merge 1 commit into
masterfrom
chavic/input-selection-and-signing
Closed

chavic wants to merge 1 commit into
masterfrom
chavic/input-selection-and-signing

Conversation

@chavic

@chavic chavic commented Jun 14, 2026 •

Copy link
Copy Markdown
Owner

Selection always contributed the smallest confirmed UTXO, and doing that on every payjoin systematically reproduces the Unnecessary Input Heuristic 2 fingerprint — the smallest input sitting below the smallest output — which advertises that the transaction is a payjoin, the opposite of the point. I replace the deterministic smallest-first order with a cryptographically secure random shuffle of the candidates, keeping the coin identity we need for reservation and signing, which removes the worst-case deterministic bias.

Separately, the guarantee that no signature leaks onto a non-contributed input was implicit: the signer signed every wallet-owned input and then leaned on four separate cleanup calls to strip signatures off everything except the contributed inputs. I collapse that cleanup — finalizing the contributed inputs, stripping finalized scriptSig/witness from every other input, and clearing all partial signatures and key paths — into a single NormalizeContributedProposalSignatures method, so the invariant is enforced in one place; this also resolves an existing TODO.

note — partial mitigation, blocked upstream

rust-payjoin's try_preserving_privacy would do proper UIH2 avoidance, but the FFI hands back an opaque InputPair with no outpoint accessor and WantsInputs exposes no PSBT, so the selected input can't be mapped back to the ReceivedCoin we need for reservation and signing. Random selection is the best standalone mitigation available; full UIH2 avoidance needs an upstream payjoin-ffi change that exposes the chosen coin or accepts an index, which is worth filing.

The receiver always contributed its smallest confirmed UTXO. A fixed smallest-first
order systematically reproduces the Unnecessary Input Heuristic 2 fingerprint (the
smallest input falling below the smallest output), which reveals that the
transaction is a payjoin. rust-payjoin's TryPreservingPrivacy would avoid this, but
the FFI returns an opaque InputPair that cannot be mapped back to the coin we need
for reservation and signing, so selection cannot be delegated to it yet. Pick a
candidate at random using a cryptographically secure shuffle instead, which removes
the worst-case deterministic bias while keeping the coin identity we rely on.

Also collapse the post-signing PSBT cleanup (finalize the contributed inputs, strip
finalized scriptSig/witness from every other input, clear all partial signatures
and key paths) into a single NormalizeContributedProposalSignatures method, so the
invariant that only the receiver's contributed inputs carry signature material is
enforced in one place rather than as four loose calls.
@chavic chavic closed this Jun 14, 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.

1 participant