Skip to content

Skip check_payment in Receiver<Monitor> if the sender is using non-segwit address#1218

Merged
arminsabouri merged 2 commits into
payjoin:masterfrom
mehmetefeumit:monitor-remove-outpoint
Dec 12, 2025
Merged

Skip check_payment in Receiver<Monitor> if the sender is using non-segwit address#1218
arminsabouri merged 2 commits into
payjoin:masterfrom
mehmetefeumit:monitor-remove-outpoint

Conversation

@mehmetefeumit

Copy link
Copy Markdown
Contributor
Pull Request Checklist

Please confirm the following before requesting review:

Overview

The previous implementation of the check_payment function assumed that if the outpoints of the Payjoin transaction have been removed from the UTXO set, it is an indication of the Payjoin being broadcasted by the sender. Moreover, it relied on the same closure to detect whether there is a double-spend attempt from the sender, if only a subset of the outpoints have been spent.

Both of the usages of the outpoint closure is incorrect. If the sender does RBF on the fallback transaction, this would change the transaction ID and cause the previous implementation to incorrectly detect double-spend. Moreover, the sender can use some of the UTXOs in the Payjoin session if they wish without necessarily "attacking" the receiver.

This change removes the outpoint closure, and instead skips the check if
the fallback transaction has any inputs which does not have witness
data. This assumes that the fallback transaction has been signed which
is a certainty at this point in the Payjoin session.

Closes #1214.

On the side: this PR also contains another small commit for completing a TODO in the unit tests.

List of Changes

  • Remove the outpoint check closure from the check_payment function and add skip if the fallback transaction is non-segwit.
  • Add a new receiver SessionOutcome value to indicate the skip.
  • Change the consumers of the function (i.e. cli, ffi) to adhere to the new signature.
  • Add unit tests.

Testing

  • Added new unit tests with P2PKH PSBTs for testing this specific scenario.

Next Step

#1211 contains a documentation update for the check_payment function. Will need to update that if and when this PR gets approved and merged, and also make sure that there are integration tests for covering p2pkh as well.

@mehmetefeumit mehmetefeumit changed the title Monitor remove outpoint Skip check_payment in Receiver<Monitor> if the sender is using non-segwit address Dec 9, 2025
@mehmetefeumit
mehmetefeumit force-pushed the monitor-remove-outpoint branch from 7117416 to 4c1789e Compare December 9, 2025 06:10
@coveralls

coveralls commented Dec 9, 2025

Copy link
Copy Markdown
Collaborator

Pull Request Test Coverage Report for Build 20173359100

Details

  • 46 of 48 (95.83%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.1%) to 83.307%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin-cli/src/app/v2/mod.rs 5 7 71.43%
Totals Coverage Status
Change from base Build 20148625592: 0.1%
Covered Lines: 9657
Relevant Lines: 11592

💛 - Coveralls

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

ConceptAck. This fixes the immediate bug in our monitoring type state.
I am assuming #1211 shold probably preceed this PR?

Comment thread payjoin/src/core/receive/v2/session.rs Outdated
@mehmetefeumit

Copy link
Copy Markdown
Contributor Author

ConceptAck. This fixes the immediate bug in our monitoring type state.
I am assuming #1211 shold probably preceed this PR?

Yeah. Going to add new test scenarios for non-SegWit to this PR once the integration test changes are merged.

@mehmetefeumit
mehmetefeumit force-pushed the monitor-remove-outpoint branch 2 times, most recently from 4fa6fb1 to fb26ec9 Compare December 10, 2025 16:32
Comment thread payjoin/src/core/receive/v2/mod.rs Outdated
Comment thread payjoin/src/core/receive/v2/mod.rs Outdated
Comment thread payjoin/src/core/receive/v2/session.rs Outdated
Comment thread payjoin/src/core/receive/v2/session.rs Outdated
Comment thread payjoin/src/core/receive/v2/mod.rs
@mehmetefeumit
mehmetefeumit force-pushed the monitor-remove-outpoint branch from fb26ec9 to d3b5192 Compare December 12, 2025 05:40

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

cACK.
Just one comment about copy displayed in the history subcommand

Comment thread payjoin-cli/src/app/v2/mod.rs Outdated
…gwit address

The previous implementation of the check_payment function assumed that
if the outpoints of the Payjoin transaction have been removed from the
UTXO set, it is an indication of the Payjoin being broadcasted by the
sender. Moreover, it relied on the same closure to detect whether there
is a double-spend attempt from the sender, if only a subset of the
outpoints have been spent.

Both of the usages of the outpoint closure is incorrect. If the sender
does RBF on the fallback transaction, this would change the transaction
ID and cause the previous implementation to incorrectly detect
double-spend. Moreover, the sender can use some of the UTXOs in the
Payjoin session if they wish without necessarily "attacking" the
receiver.

This change removes the outpoint closure, and instead skips the check if
the fallback transaction has any inputs which does not have witness
data. This assumes that the fallback transaction has been signed which
is a certainty at this point in the Payjoin session.
@mehmetefeumit
mehmetefeumit force-pushed the monitor-remove-outpoint branch from d3b5192 to fb368c0 Compare December 12, 2025 16:36

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

utACK fb368c0

@arminsabouri
arminsabouri merged commit a02bfce into payjoin:master Dec 12, 2025
16 checks passed
@mehmetefeumit
mehmetefeumit deleted the monitor-remove-outpoint branch December 13, 2025 05:20
@DanGould

Copy link
Copy Markdown
Member

Has there been any discussion about not allowing this state

ReceiverSessionOutcome::PayjoinProposalSent =>
"Payjoin proposal sent, skipping monitoring as the sender is spending non-SegWit inputs"`

And instead requiring that clients that support non-SegWit inputs instead rely on an address index like esplora, allowing the state machine and clients to be simplified. I understand that this would limit our reference implementation, but the index is a sort of prerequisite for any full on wallet that we've integrated with in the past.

@mehmetefeumit

Copy link
Copy Markdown
Contributor Author

We did briefly talk about how since not all clients have an address index, we could not reliably change this into something which would provide consistent experience. If I got it right, your suggestion is to only remove the new state, and change the documentation, etc. around the state to reference an address index usage to solve the non-SegWit problem.

That makes sense to me. Trying to wrap my head around how it looks like in two different cases:

  1. In the case of the reference implementation in the CLI (or any other RPC usages), the monitor function would simply return a Success if the original proposal had non-SegWit inputs, and otherwise continue monitoring the network.
  2. In the case of clients with address index, I need to understand whether the closure API would be consistent among different clients, and if not, what is the signature for those closure parameters to allow different clients to do the same thing.

For the second point, let me know if you have any information! Otherwise I understand that real world usages will have address index capabilities to make this possible. Let me create an issue for it to move the discussion there.

@DanGould

DanGould commented Dec 24, 2025

Copy link
Copy Markdown
Member
  1. In the case of the reference implementation in the CLI (or any other RPC usages), the monitor function would simply return a Success if the original proposal had non-SegWit inputs, and otherwise continue monitoring the network.

I think we can simplify by disallowing non-SegWit inputs in the reference implmentation.

  1. In the case of clients with address index, I need to understand whether the closure API would be consistent among different clients, and if not, what is the signature for those closure parameters to allow different clients to do the same thing.

I think the closure would change to be |address| instead of |txid|, in the case of an address index, or |outpoint| or |script| with an outpoint or script index. More research needs to be done to enumerate the indexes wallets and services are relying on.

DanGould added a commit to DanGould/rust-payjoin that referenced this pull request Jul 17, 2026
The persisted SessionEvent format freezes at the payjoin-cli 1.0
database-backwards-compatibility commitment, so reshape the receiver's
terminal SessionOutcome while a format break is still free.

Change Success(Vec<(ScriptBuf, Witness)>) to Success(Txid). The
witness payload was recorded so the receiver could reconstruct the
final Payjoin on the non-segwit path, but payjoin#1218 removed that path
entirely, leaving the enum payload as dead code. The Receiver settlement
classifier (payjoin#1692) will restore non-segwit detection, which would make
the payload live again, but sufficient record of a settlement is the txid
of the transaction that settled it rather than the witness list. The
wallet that observed the spend already holds the full transaction.
Recording Success(Txid) also keeps the terminal variants consistent
where the outcome for the transaction we recognize carries the same
evidence as the one we do not.

Add Other(Txid), mapped to SessionStatus::Failed, for contested
outpoints settled by an unrecognized transaction. Nothing produces it
yet. The reason to reserve it now is API semver, not data
compatibility. 8c92050 deliberately kept SessionOutcome exhaustive.
A wildcard match arm has no meaningful semantics for a terminal outcome,
and the compile error on a new variant is a feature. So adding a
variant after 1.0 would be a semver-breaking change that we want. Naming
Other before the freeze makes the settlement classifier additive by
construction while keeping the enum exhaustive. (Forward
compatibility of the persisted data is explicitly not a goal. See the
persist module docs, we want unknown state to error.)

Update payjoin-cli to match the new shapes exhaustively. no wildcard
arms, per 8c92050 surfacing the settlement txid in the history and
cancel output, and pin the sealed JSON encodings of the terminal
events in a serialization-shape test. StatusText becomes String rather
than &'static str in order to accomodate the txid in format!.
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.

Detecting non-segwit proposal broadcast

4 participants