Skip to content

Sign receiver inputs inside rust-payjoin finalize callback - #64

Merged
ValeraFinebits merged 3 commits into
ValeraFinebits:masterfrom
chavic:chavic/consolidate-signature-stripping
Jun 24, 2026
Merged

ValeraFinebits merged 3 commits into
ValeraFinebits:masterfrom
chavic:chavic/consolidate-signature-stripping

Conversation

@chavic

@chavic chavic commented Jun 18, 2026 •

Copy link
Copy Markdown
Collaborator

This updates the receiver signing path so the plugin no longer signs the whole proposal and then strips sender input data afterwards.

The receiver now resolves the hot-wallet account key asynchronously, passes a synchronous ProcessPsbt callback into rust-payjoin's FinalizeProposal flow, and signs only the receiver coins that were contributed by this session. The callback fails if a persisted contributed input is missing from the PSBT or if that input cannot be finalized.

This leaves sender inputs untouched by the plugin signer and relies on rust-payjoin's finalize path for proposal cleanup instead of maintaining a separate manual normalization pipeline in the plugin.

The latest update also moves the callback signer tests into PayjoinReceiverProposalSignerTests and adds targeted coverage for the missing-input and cannot-finalize failure paths requested in review.

@chavic
chavic marked this pull request as draft June 18, 2026 09:44
@DanGould

Copy link
Copy Markdown

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.

What prompted you to view manual stripping as something that needed to be done here?

@ValeraFinebits

Copy link
Copy Markdown
Owner

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.

What prompted you to view manual stripping as something that needed to be done here?

@DanGould I think that question is really for me rather than for @chavic.

The underlying post-signing normalization was already in the plugin before this PR. I added it originally because without that normalization, the proposal PSBT was being rejected by the current rust-payjoin checks. What @chavic changed here was just to consolidate those existing steps into NormalizeContributedProposalSignatures() and make the invariant explicit in one place.

@chavic

chavic commented Jun 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.

What prompted you to view manual stripping as something that needed to be done here?

@DanGould Sftlr, 🤔 That actually simplifies this a bit more. Was digging into BTCPay's SignAll method and trying to understand why it was necessary in the first place. Knowing the lib does this is good (new to me, but also makes it less of an issue than initially assumed)... Currently looking into a way to do this without having to use SignAll

@chavic
chavic force-pushed the chavic/consolidate-signature-stripping branch from ac0eafc to e678da6 Compare June 18, 2026 14:29
@ValeraFinebits

Copy link
Copy Markdown
Owner

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.
What prompted you to view manual stripping as something that needed to be done here?

@DanGould Sftlr, 🤔 That actually simplifies this a bit more. Was digging into BTCPay's SignAll method and trying to understand why it was necessary to use it in the first place, knowing the lib does this is good... Currently looking into a way to do this without having to use SignAll

@chavic I think removing SignAll is a good direction here, but the tests should catch up with that behavioral change. Since this PR now changes the signing path itself, I’d like to see targeted coverage for the per-input signing flow.

@ValeraFinebits

Copy link
Copy Markdown
Owner

@ValeraFinebits is https://github.com/ValeraFinebits/btcpayserver-payjoin-plugin/actions/runs/27766668705/job/82155171759?pr=64 the kind of issue you were talking about here, or this something new

@chavic Do these tests pass for you locally?

@chavic

chavic commented Jun 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

@ValeraFinebits is ValeraFinebits/btcpayserver-payjoin-plugin/actions/runs/27766668705/job/82155171759?pr=64 the kind of issue you were talking about here, or this something new

@chavic Do these tests pass for you locally?

The issue seemed to be in ConcurrentContentionDoesNotPoisonSubsequentPayjoinBatch

@ValeraFinebits

Copy link
Copy Markdown
Owner

@ValeraFinebits is ValeraFinebits/btcpayserver-payjoin-plugin/actions/runs/27766668705/job/82155171759?pr=64 the kind of issue you were talking about here, or this something new

@chavic Do these tests pass for you locally?

The issue seemed to be in ConcurrentContentionDoesNotPoisonSubsequentPayjoinBatch

@chavic I think this may be the same flaky test tracked in #55
I've added this run there as well for investigation.

@chavic

chavic commented Jun 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.

What prompted you to view manual stripping as something that needed to be done here?

@ValeraFinebits @DanGould, looking into addressing this directly, it seems both this and the previous approaches can now be seen as compensations for the root cause. I still need to look into what forced this path

@ValeraFinebits

Copy link
Copy Markdown
Owner

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.
What prompted you to view manual stripping as something that needed to be done here?

@ValeraFinebits @DanGould, looking into addressing this directly, it seems both this and the previous approaches can now be seen as compensations for the root cause. I still need to look into what forced this path

To be clear, the immediate root cause here was that I originally went through SignAll(). That's what made the normalization necessary in the first place.
That said, if you think there's a cleaner or more correct path, feel free to keep digging.

@chavic

chavic commented Jun 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

This stripping isn't done by the library already? If it isn't, it should be addressed there so that other implementations don't have the same problem. Confirm against the BIP77/BIP78 checklists.
What prompted you to view manual stripping as something that needed to be done here?

@ValeraFinebits @DanGould, looking into addressing this directly, it seems both this and the previous approaches can now be seen as compensations for the root cause. I still need to look into what forced this path

To be clear, the immediate root cause here was that I originally went through SignAll(). That's what made the normalization necessary in the first place. That said, if you think there's a cleaner or more correct path, feel free to keep digging.

Basically, just going to try and take advantage of the library as much as possible, 00cce6f tries to do that

@chavic
chavic marked this pull request as ready for review June 22, 2026 11:53
@chavic
chavic requested a review from ValeraFinebits June 22, 2026 11:54
@chavic

chavic commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

@ValeraFinebits, I think this is ready for another review

@ValeraFinebits ValeraFinebits left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The PR title and description are stale. They still describe the intermediate NormalizeContributedProposalSignatures approach, but the final code now signs inside the rust-payjoin callback and removes the manual stripping entirely. Please update the PR metadata to match the actual final implementation.

@ValeraFinebits

Copy link
Copy Markdown
Owner

@ValeraFinebits, I think this is ready for another review

@chavic Done.

@chavic chavic changed the title Consolidate receiver proposal signature stripping Sign receiver inputs inside rust-payjoin finalize callback Jun 24, 2026
chavic added 3 commits June 24, 2026 15:03
The signer signed the whole proposal with SignAll over the store's derivation,
which signs every wallet-owned input the receiver has keys for — including any the
sender placed in the original PSBT — and then relied on cleanup to strip the
signatures back off everything except the contributed inputs. Sign each contributed
input individually instead, addressed by its outpoint and signed with the key
derived from that coin's key path, so foreign and sender inputs are never signed in
the first place. This mirrors BTCPayServer's core payjoin endpoint, which signs each
selected UTXO individually.

The post-signing normalization (finalize the contributed inputs, strip any
sender-provided signatures, clear partial signatures and key paths) stays, now as a
single NormalizeContributedProposalSignatures step.
The signer fetched the proposal PSBT through the public PsbtToSign() accessor,
signed it externally, and handed it back through a pass-through finalize_proposal
callback that ignored the PSBT the library supplied. That public accessor returns
the raw payjoin PSBT with the sender's now-invalid signatures still attached, so
the plugin had to strip those (plus partial signatures and key paths) by hand
afterwards.

Move signing into the finalize_proposal callback so it operates on the PSBT
rust-payjoin hands the wallet — which already has the sender signatures cleared —
and sign only the receiver's contributed inputs there. The library's prepare_psbt
then drops partial signatures and key paths, so the manual stripping is removed
entirely. The account key is resolved before finalize because the callback is
synchronous.

Because the signed PSBT now only exists after the callback runs, the expected
settlement transaction is recorded from the resulting proposal after Save rather
than from a pre-signed PSBT beforehand.
@chavic
chavic force-pushed the chavic/consolidate-signature-stripping branch from 68b4589 to 8314dcb Compare June 24, 2026 13:05
@ValeraFinebits
ValeraFinebits self-requested a review June 24, 2026 13:25
@ValeraFinebits
ValeraFinebits merged commit 5173ea6 into ValeraFinebits:master Jun 24, 2026
4 checks passed
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.

3 participants